first commit
This commit is contained in:
@@ -0,0 +1,823 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Base\Exceptions\WarningException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Akeeba\Engine\Util\FileCloseAware;
|
||||
use Akeeba\Engine\Util\FileSystem;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Abstract parent class of all archiver engines
|
||||
*/
|
||||
abstract class Base
|
||||
{
|
||||
use FileCloseAware;
|
||||
|
||||
/** @var string The archive's comment. It's currently used ONLY in the ZIP file format */
|
||||
protected $_comment;
|
||||
|
||||
/** @var Filesystem Filesystem utilities object */
|
||||
protected $fsUtils = null;
|
||||
|
||||
/** @var resource JPA transformation source handle */
|
||||
private $_xform_fp;
|
||||
|
||||
/** @var int The total size of the source JPA file */
|
||||
private $totalSourceJPASize = 0;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup (unserialization) function
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single file in the archive
|
||||
*
|
||||
* @param string $file The absolute path to the file to add
|
||||
* @param string $removePath Path to remove from $file
|
||||
* @param string $addPath Path to prepend to $file
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public final function addFile($file, $removePath = '', $addPath = '')
|
||||
{
|
||||
$storedName = $this->addRemovePaths($file, $removePath, $addPath);
|
||||
|
||||
$this->addFileRenamed($file, $storedName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of files into the archive, removing $removePath from the
|
||||
* file names and adding $addPath to them.
|
||||
*
|
||||
* @param array $fileList A simple string array of filepaths to include
|
||||
* @param string $removePath Paths to remove from the filepaths
|
||||
* @param string $addPath Paths to add in front of the filepaths
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public final function addFileList(&$fileList, $removePath = '', $addPath = '')
|
||||
{
|
||||
if (!is_array($fileList))
|
||||
{
|
||||
Factory::getLog()->warning('addFileList called without a file list array');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($fileList as $file)
|
||||
{
|
||||
$this->addFile($file, $removePath, $addPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the archive, with a name that's different from the source
|
||||
* filename
|
||||
*
|
||||
* @param string $sourceFile Absolute path to the source file
|
||||
* @param string $targetFile Relative filename to store in archive
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function addFileRenamed($sourceFile, $targetFile)
|
||||
{
|
||||
$mb_encoding = '8bit';
|
||||
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
$mb_encoding = mb_internal_encoding();
|
||||
mb_internal_encoding('ISO-8859-1');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->_addFile(false, $sourceFile, $targetFile);
|
||||
}
|
||||
catch (WarningException $e)
|
||||
{
|
||||
Factory::getLog()->warning($e->getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
mb_internal_encoding($mb_encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the archive, given the stored name and its contents
|
||||
*
|
||||
* @param string $fileName The base file name
|
||||
* @param string $addPath The relative path to prepend to file name
|
||||
* @param string $virtualContent The contents of the file to be archived
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public final function addFileVirtual($fileName, $addPath, &$virtualContent)
|
||||
{
|
||||
$storedName = $this->addRemovePaths($fileName, '', $addPath);
|
||||
$mb_encoding = '8bit';
|
||||
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
$mb_encoding = mb_internal_encoding();
|
||||
mb_internal_encoding('ISO-8859-1');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->_addFile(true, $virtualContent, $storedName);
|
||||
}
|
||||
catch (WarningException $e)
|
||||
{
|
||||
Factory::getLog()->warning($e->getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
mb_internal_encoding($mb_encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the archive, given the stored name and its contents
|
||||
*
|
||||
* @param string $fileName The base file name
|
||||
* @param string $addPath The relative path to prepend to file name
|
||||
* @param string $virtualContent The contents of the file to be archived
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated 7.0.0
|
||||
*/
|
||||
public final function addVirtualFile($fileName, $addPath, &$virtualContent)
|
||||
{
|
||||
Factory::getLog()->debug('DEPRECATED: addVirtualFile() has been renamed to addFileVirtual().');
|
||||
|
||||
$this->addFileVirtual($fileName, $addPath, $virtualContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes whatever finalization is needed for the archive to be considered
|
||||
* complete and useful (or, generally, clean up)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function finalize();
|
||||
|
||||
/**
|
||||
* Returns a string with the extension (including the dot) of the files produced
|
||||
* by this class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getExtension();
|
||||
|
||||
/**
|
||||
* Initialises the archiver class, creating the archive from an existent
|
||||
* installer's JPA archive. MUST BE OVERRIDEN BY CHILDREN CLASSES.
|
||||
*
|
||||
* @param string $targetArchivePath Absolute path to the generated archive
|
||||
* @param array $options A named key array of options (optional)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function initialize($targetArchivePath, $options = []);
|
||||
|
||||
/**
|
||||
* Notifies the engine on the backup comment and converts it to plain text for
|
||||
* inclusion in the archive file, if applicable.
|
||||
*
|
||||
* @param string $comment The archive's comment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
// First, sanitize the comment in a text-only format
|
||||
$comment = str_replace("\n", " ", $comment); // Replace newlines with spaces
|
||||
$comment = str_replace("<br>", "\n", $comment); // Replace HTML4 <br> with single newlines
|
||||
$comment = str_replace("<br/>", "\n", $comment); // Replace HTML4 <br> with single newlines
|
||||
$comment = str_replace("<br />", "\n", $comment); // Replace HTML <br /> with single newlines
|
||||
$comment = str_replace("</p>", "\n\n", $comment); // Replace paragraph endings with double newlines
|
||||
$comment = str_replace("<b>", "*", $comment); // Replace bold with star notation
|
||||
$comment = str_replace("</b>", "*", $comment); // Replace bold with star notation
|
||||
$comment = str_replace("<i>", "_", $comment); // Replace italics with underline notation
|
||||
$comment = str_replace("</i>", "_", $comment); // Replace italics with underline notation
|
||||
$this->_comment = strip_tags($comment, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a JPA archive (containing an installer) to the native archive format
|
||||
* of the class. It actually extracts the source JPA in memory and instructs the
|
||||
* class to include each extracted file.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param integer $index The index in the source JPA archive's list currently in use
|
||||
* @param integer $offset The source JPA archive's offset to use
|
||||
*
|
||||
* @return array|bool False if an error occurred, return array otherwise
|
||||
*/
|
||||
public function transformJPA($index, $offset)
|
||||
{
|
||||
$xform_source = null;
|
||||
|
||||
// Do we have to open the file?
|
||||
if (!$this->_xform_fp)
|
||||
{
|
||||
// Get the source path
|
||||
$registry = Factory::getConfiguration();
|
||||
$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');
|
||||
|
||||
// Fetch the name of the installer image
|
||||
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
|
||||
$xform_source = Platform::getInstance()->get_installer_images_path() .
|
||||
'/foobar.jpa'; // We need this as a "safe fallback"
|
||||
|
||||
// Try to find a sane default if we are not given a valid embedded installer
|
||||
if (!array_key_exists($embedded_installer, $installerDescriptors))
|
||||
{
|
||||
$embedded_installer = 'angie';
|
||||
|
||||
if (!array_key_exists($embedded_installer, $installerDescriptors))
|
||||
{
|
||||
$allInstallers = array_keys($installerDescriptors);
|
||||
|
||||
foreach ($allInstallers as $anInstaller)
|
||||
{
|
||||
if ($anInstaller == 'none')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$embedded_installer = $anInstaller;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($embedded_installer, $installerDescriptors))
|
||||
{
|
||||
$packages = $installerDescriptors[$embedded_installer]['package'] ?? '';
|
||||
$langPacks = $installerDescriptors[$embedded_installer]['language'] ?? '';
|
||||
|
||||
if (empty($packages))
|
||||
{
|
||||
// No installer package specified. Pretend we are done!
|
||||
$retArray = [
|
||||
"filename" => '', // File name extracted
|
||||
"data" => '', // File data
|
||||
"index" => 0, // How many source JPA files I have
|
||||
"offset" => 0, // Offset in JPA file
|
||||
"skip" => false, // Skip this?
|
||||
"done" => true, // Are we done yet?
|
||||
"filesize" => 0,
|
||||
];
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
|
||||
$packages = explode(',', $packages);
|
||||
$langPacks = explode(',', $langPacks);
|
||||
$this->totalSourceJPASize = 0;
|
||||
$pathPrefix = Platform::getInstance()->get_installer_images_path() . '/';
|
||||
|
||||
foreach ($packages as $package)
|
||||
{
|
||||
$filePath = $pathPrefix . $package;
|
||||
$this->totalSourceJPASize += (int) @filesize($filePath);
|
||||
}
|
||||
|
||||
foreach ($langPacks as $langPack)
|
||||
{
|
||||
$filePath = $pathPrefix . $langPack;
|
||||
|
||||
if (!is_file($filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$packages[] = $langPack;
|
||||
$this->totalSourceJPASize += (int) @filesize($filePath);
|
||||
}
|
||||
|
||||
if (count($packages) < $index)
|
||||
{
|
||||
throw new RuntimeException(__CLASS__ . ":: Installer package index $index not found for embedded installer $embedded_installer");
|
||||
}
|
||||
|
||||
$package = $packages[$index];
|
||||
|
||||
// A package is specified, use it!
|
||||
$xform_source = $pathPrefix . $package;
|
||||
}
|
||||
|
||||
// 2.3: Try to use sane default if the indicated installer doesn't exist
|
||||
if (!is_null($xform_source) && !file_exists($xform_source) && (basename($xform_source) != 'angie.jpa'))
|
||||
{
|
||||
throw new RuntimeException(__CLASS__ . ":: Installer package $xform_source of embedded installer $embedded_installer not found. Please go to the configuration page, select an Embedded Installer, save the configuration and try backing up again.");
|
||||
}
|
||||
|
||||
// Try opening the file
|
||||
if (!is_null($xform_source) && file_exists($xform_source))
|
||||
{
|
||||
$this->_xform_fp = @fopen($xform_source, 'r');
|
||||
|
||||
if ($this->_xform_fp === false)
|
||||
{
|
||||
throw new RuntimeException(__CLASS__ . ":: Can't seed archive with installer package " . $xform_source);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException(__CLASS__ . ":: Installer package " . $xform_source . " does not exist!");
|
||||
}
|
||||
}
|
||||
|
||||
$headerDataLength = 0;
|
||||
|
||||
if (!$offset)
|
||||
{
|
||||
// First run detected!
|
||||
Factory::getLog()->debug('Initializing with JPA package ' . $xform_source);
|
||||
|
||||
// Skip over the header and check no problem exists
|
||||
$offset = $this->_xformReadHeader();
|
||||
|
||||
if ($offset === false)
|
||||
{
|
||||
throw new RuntimeException('JPA package file was not read');
|
||||
}
|
||||
|
||||
$headerDataLength = $offset;
|
||||
}
|
||||
|
||||
$ret = $this->_xformExtract($offset);
|
||||
|
||||
$ret['index'] = $index;
|
||||
|
||||
if (is_array($ret))
|
||||
{
|
||||
$ret['chunkProcessed'] = $headerDataLength + $ret['offset'] - $offset;
|
||||
$offset = $ret['offset'];
|
||||
|
||||
if (!$ret['skip'] && !$ret['done'])
|
||||
{
|
||||
Factory::getLog()->debug(' Adding ' . $ret['filename'] . '; Next offset:' . $offset);
|
||||
|
||||
$this->addFileVirtual($ret['filename'], '', $ret['data']);
|
||||
}
|
||||
elseif ($ret['done'])
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');
|
||||
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
|
||||
$packages = $installerDescriptors[$embedded_installer]['package'];
|
||||
$packages = explode(',', $packages);
|
||||
$pathPrefix = Platform::getInstance()->get_installer_images_path() . '/';
|
||||
$langPacks = $installerDescriptors[$embedded_installer]['language'];
|
||||
$langPacks = explode(',', $langPacks);
|
||||
|
||||
foreach ($langPacks as $langPack)
|
||||
{
|
||||
$filePath = $pathPrefix . $langPack;
|
||||
|
||||
if (!is_file($filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$packages[] = $langPack;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug(' Done with package ' . $packages[$index]);
|
||||
|
||||
if (count($packages) > ($index + 1))
|
||||
{
|
||||
$ret['done'] = false;
|
||||
$ret['index'] = $index + 1;
|
||||
$ret['offset'] = 0;
|
||||
$this->_xform_fp = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->debug(' Done with installer seeding.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$reason = ' Skipping ' . $ret['filename'];
|
||||
Factory::getLog()->debug($reason);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException('JPA extraction returned FALSE. The installer image is corrupt.');
|
||||
}
|
||||
|
||||
if ($ret['done'])
|
||||
{
|
||||
// We are finished! Close the file
|
||||
$this->conditionalFileClose($this->_xform_fp);
|
||||
Factory::getLog()->debug('Initializing with JPA package has finished');
|
||||
}
|
||||
|
||||
$ret['filesize'] = $this->totalSourceJPASize;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common code which gets called on instance creation or wake-up (unserialization)
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __bootstrap_code()
|
||||
{
|
||||
$this->fsUtils = Factory::getFilesystemTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* The most basic file transaction: add a single entry (file or directory) to
|
||||
* the archive.
|
||||
*
|
||||
* @param boolean $isVirtual If true, the next parameter contains file data instead of a file name
|
||||
* @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual
|
||||
* is true
|
||||
* @param string $targetName The (relative) file name under which to store the file in the archive
|
||||
*
|
||||
* @return boolean True on success, false otherwise. DEPRECATED: Use exceptions instead.
|
||||
*
|
||||
* @throws WarningException When there's a warning (the backup integrity is NOT compromised)
|
||||
* @throws ErrorException When there's an error (the backup integrity is compromised – backup dead)
|
||||
*/
|
||||
abstract protected function _addFile($isVirtual, &$sourceNameOrData, $targetName);
|
||||
|
||||
/**
|
||||
* This function indicates if the path $p_path is under the $p_dir tree. Or,
|
||||
* said in an other way, if the file or sub-dir $p_path is inside the dir
|
||||
* $p_dir.
|
||||
* The function indicates also if the path is exactly the same as the dir.
|
||||
* This function supports path with duplicated '/' like '//', but does not
|
||||
* support '.' or '..' statements.
|
||||
*
|
||||
* Copied verbatim from pclZip library
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $p_dir Source tree
|
||||
* @param string $p_path Check if this is part of $p_dir
|
||||
*
|
||||
* @return integer 0 if $p_path is not inside directory $p_dir,
|
||||
* 1 if $p_path is inside directory $p_dir
|
||||
* 2 if $p_path is exactly the same as $p_dir
|
||||
*/
|
||||
private function _PathInclusion($p_dir, $p_path)
|
||||
{
|
||||
$v_result = 1;
|
||||
|
||||
// ----- Explode dir and path by directory separator
|
||||
$v_list_dir = explode("/", $p_dir);
|
||||
$v_list_dir_size = count($v_list_dir);
|
||||
$v_list_path = explode("/", $p_path);
|
||||
$v_list_path_size = count($v_list_path);
|
||||
|
||||
// ----- Study directories paths
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
|
||||
while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result))
|
||||
{
|
||||
// ----- Look for empty dir (path reduction)
|
||||
if ($v_list_dir[$i] == '')
|
||||
{
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($v_list_path[$j] == '')
|
||||
{
|
||||
$j++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ----- Compare the items
|
||||
if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != ''))
|
||||
{
|
||||
$v_result = 0;
|
||||
}
|
||||
|
||||
// ----- Next items
|
||||
$i++;
|
||||
$j++;
|
||||
}
|
||||
|
||||
// ----- Look if everything seems to be the same
|
||||
if ($v_result)
|
||||
{
|
||||
// ----- Skip all the empty items
|
||||
while (($j < $v_list_path_size) && ($v_list_path[$j] == ''))
|
||||
{
|
||||
$j++;
|
||||
}
|
||||
|
||||
while (($i < $v_list_dir_size) && ($v_list_dir[$i] == ''))
|
||||
{
|
||||
$i++;
|
||||
}
|
||||
|
||||
if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size))
|
||||
{
|
||||
// ----- There are exactly the same
|
||||
$v_result = 2;
|
||||
}
|
||||
else if ($i < $v_list_dir_size)
|
||||
{
|
||||
// ----- The path is shorter than the dir
|
||||
$v_result = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Return
|
||||
return $v_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a file from the JPA archive and returns an in-memory array containing it
|
||||
* and its file data. The data returned is an array, consisting of the following keys:
|
||||
* "filename" => relative file path stored in the archive
|
||||
* "data" => file data
|
||||
* "offset" => next offset to use
|
||||
* "skip" => if this is not a file, just skip it...
|
||||
* "done" => No more files left in archive
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param integer $offset The absolute data offset from archive's header
|
||||
*
|
||||
* @return array|bool See description for more information
|
||||
*/
|
||||
private function &_xformExtract($offset)
|
||||
{
|
||||
$false = false; // Used to return false values in case an error occurs
|
||||
|
||||
// Generate a return array
|
||||
$retArray = [
|
||||
"filename" => '', // File name extracted
|
||||
"data" => '', // File data
|
||||
"offset" => 0, // Offset in ZIP file
|
||||
"skip" => false, // Skip this?
|
||||
"done" => false // Are we done yet?
|
||||
];
|
||||
|
||||
// If we can't open the file, return an error condition
|
||||
if ($this->_xform_fp === false)
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
// Go to the offset specified
|
||||
if (!fseek($this->_xform_fp, $offset) == 0)
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
// Get and decode Entity Description Block
|
||||
$signature = fread($this->_xform_fp, 3);
|
||||
|
||||
// Check signature
|
||||
if ($signature == 'JPF')
|
||||
{
|
||||
// This a JPA Entity Block. Process the header.
|
||||
|
||||
// Read length of EDB and of the Entity Path Data
|
||||
$length_array = unpack('vblocksize/vpathsize', fread($this->_xform_fp, 4));
|
||||
// Read the path data
|
||||
$file = fread($this->_xform_fp, $length_array['pathsize']);
|
||||
// Read and parse the known data portion
|
||||
$bin_data = fread($this->_xform_fp, 14);
|
||||
$header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data);
|
||||
// Read any unknwon data
|
||||
$restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']);
|
||||
|
||||
if ($restBytes > 0)
|
||||
{
|
||||
$junk = fread($this->_xform_fp, $restBytes);
|
||||
}
|
||||
|
||||
$compressionType = $header_data['compression'];
|
||||
|
||||
// Populate the return array
|
||||
$retArray['filename'] = $file;
|
||||
$retArray['skip'] = ($header_data['compsize'] == 0); // Skip over directories
|
||||
|
||||
switch ($header_data['type'])
|
||||
{
|
||||
case 0:
|
||||
// directory
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// file
|
||||
switch ($compressionType)
|
||||
{
|
||||
case 0: // No compression
|
||||
if ($header_data['compsize'] > 0) // 0 byte files do not have data to be read
|
||||
{
|
||||
$retArray['data'] = fread($this->_xform_fp, $header_data['compsize']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // GZip compression
|
||||
$zipData = fread($this->_xform_fp, $header_data['compsize']);
|
||||
$retArray['data'] = gzinflate($zipData);
|
||||
break;
|
||||
|
||||
case 2: // BZip2 compression
|
||||
$zipData = fread($this->_xform_fp, $header_data['compsize']);
|
||||
$retArray['data'] = bzdecompress($zipData);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is not a file header. This means we are done.
|
||||
$retArray['done'] = true;
|
||||
}
|
||||
|
||||
$retArray['offset'] = ftell($this->_xform_fp);
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over the JPA header entry and returns the offset file data starts from
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return boolean|integer False on failure, offset otherwise
|
||||
*/
|
||||
private function _xformReadHeader()
|
||||
{
|
||||
// Fail for unreadable files
|
||||
if ($this->_xform_fp === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go to the beggining of the file
|
||||
rewind($this->_xform_fp);
|
||||
|
||||
// Read the signature
|
||||
$sig = fread($this->_xform_fp, 3);
|
||||
|
||||
// Not a JPA Archive?
|
||||
if ($sig != 'JPA')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and parse header length
|
||||
$header_length_array = unpack('v', fread($this->_xform_fp, 2));
|
||||
$header_length = $header_length_array[1];
|
||||
|
||||
// Read and parse the known portion of header data (14 bytes)
|
||||
$bin_data = fread($this->_xform_fp, 14);
|
||||
$header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data);
|
||||
|
||||
// Load any remaining header data (forward compatibility)
|
||||
$rest_length = $header_length - 19;
|
||||
|
||||
if ($rest_length > 0)
|
||||
{
|
||||
$junk = fread($this->_xform_fp, $rest_length);
|
||||
}
|
||||
|
||||
return ftell($this->_xform_fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the $p_remove_dir from $p_filename, while prepending it with $p_add_dir.
|
||||
* Largely based on code from the pclZip library.
|
||||
*
|
||||
* @param string $p_filename The absolute file name to treat
|
||||
* @param string $p_remove_dir The path to remove
|
||||
* @param string $p_add_dir The path to prefix the treated file name with
|
||||
*
|
||||
* @return string The treated file name
|
||||
*/
|
||||
private function addRemovePaths($p_filename, $p_remove_dir, $p_add_dir)
|
||||
{
|
||||
$p_filename = $this->fsUtils->TranslateWinPath($p_filename);
|
||||
$p_remove_dir = ($p_remove_dir == '') ? '' :
|
||||
$this->fsUtils->TranslateWinPath($p_remove_dir); //should fix corrupt backups, fix by nicholas
|
||||
|
||||
$v_stored_filename = $p_filename;
|
||||
|
||||
if (!($p_remove_dir == ""))
|
||||
{
|
||||
if (substr($p_remove_dir, -1) != '/')
|
||||
{
|
||||
$p_remove_dir .= "/";
|
||||
}
|
||||
|
||||
if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./"))
|
||||
{
|
||||
if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./"))
|
||||
{
|
||||
$p_remove_dir = "./" . $p_remove_dir;
|
||||
}
|
||||
|
||||
if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./"))
|
||||
{
|
||||
$p_remove_dir = substr($p_remove_dir, 2);
|
||||
}
|
||||
}
|
||||
|
||||
$v_compare = $this->_PathInclusion($p_remove_dir, $p_filename);
|
||||
|
||||
if ($v_compare > 0)
|
||||
{
|
||||
if ($v_compare == 2)
|
||||
{
|
||||
$v_stored_filename = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
$v_stored_filename =
|
||||
substr($p_filename, (function_exists('mb_strlen') ? mb_strlen($p_remove_dir, '8bit') :
|
||||
strlen($p_remove_dir)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$v_stored_filename = $p_filename;
|
||||
}
|
||||
|
||||
if (!($p_add_dir == ""))
|
||||
{
|
||||
if (substr($p_add_dir, -1) == "/")
|
||||
{
|
||||
$v_stored_filename = $p_add_dir . $v_stored_filename;
|
||||
}
|
||||
else
|
||||
{
|
||||
$v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
|
||||
}
|
||||
}
|
||||
|
||||
return $v_stored_filename;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Base\Exceptions\WarningException;
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
if (!defined('AKEEBA_CHUNK'))
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$chunksize = $configuration->get('engine.archiver.common.chunk_size', 1048576);
|
||||
define('AKEEBA_CHUNK', $chunksize);
|
||||
}
|
||||
|
||||
if (!function_exists('aksubstr'))
|
||||
{
|
||||
/**
|
||||
* Attempt to use mbstring for getting parts of strings
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $start
|
||||
* @param int|null $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function aksubstr($string, $start, $length = null)
|
||||
{
|
||||
return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') :
|
||||
substr($string, $start, $length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for custom archiver implementations
|
||||
*/
|
||||
abstract class BaseArchiver extends BaseFileManagement
|
||||
{
|
||||
/** @var array The last part which has been finalized and waits to be post-processed */
|
||||
public $finishedPart = [];
|
||||
|
||||
/** @var resource File pointer to the archive being currently written to */
|
||||
protected $fp = null;
|
||||
|
||||
/** @var resource File pointer to the archive's central directory file (for ZIP) */
|
||||
protected $cdfp = null;
|
||||
|
||||
/** @var string The name of the file holding the archive's data, which becomes the final archive */
|
||||
protected $_dataFileName;
|
||||
|
||||
/** @var string Archive full path without extension */
|
||||
protected $dataFileNameWithoutExtension = '';
|
||||
|
||||
/** @var bool Should I store symlinks as such (no dereferencing?) */
|
||||
protected $storeSymlinkTarget = false;
|
||||
|
||||
/** @var int Part size for split archives, in bytes */
|
||||
protected $partSize = 0;
|
||||
|
||||
/** @var bool Should I use Split ZIP? */
|
||||
protected $useSplitArchive = false;
|
||||
|
||||
/** @var int Permissions for the backup archive part files */
|
||||
protected $permissions = null;
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being serialized
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function _onSerialize()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being destroyed
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new archive part file (but does NOT open it for writing)
|
||||
*
|
||||
* @param bool $finalPart True if this is the final part
|
||||
*
|
||||
* @return bool False if creating a new part fails
|
||||
*/
|
||||
abstract protected function createNewPartFile($finalPart = false);
|
||||
|
||||
/**
|
||||
* Create a new part file and open it for writing
|
||||
*
|
||||
* @param bool $finalPart Is this the final part?
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function createAndOpenNewPart($finalPart = false)
|
||||
{
|
||||
@$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
|
||||
// Not enough space on current part, create new part
|
||||
if (!$this->createNewPartFile($finalPart))
|
||||
{
|
||||
$extension = $this->getExtension();
|
||||
$extension = ltrim(strtoupper($extension), '.');
|
||||
|
||||
throw new ErrorException("Could not create new $extension part file " . basename($this->_dataFileName));
|
||||
}
|
||||
|
||||
$this->openArchiveForOutput(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new backup archive
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
protected function createNewBackupArchive()
|
||||
{
|
||||
Factory::getLog()->debug(__CLASS__ . " :: Killing old archive");
|
||||
|
||||
$this->fp = $this->fopen($this->_dataFileName, "w");
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
if (file_exists($this->_dataFileName))
|
||||
{
|
||||
@unlink($this->_dataFileName);
|
||||
}
|
||||
|
||||
@touch($this->_dataFileName);
|
||||
@chmod($this->_dataFileName, 0666);
|
||||
|
||||
$this->fp = $this->fopen($this->_dataFileName, "w");
|
||||
|
||||
if ($this->fp !== false)
|
||||
{
|
||||
throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
|
||||
}
|
||||
}
|
||||
|
||||
@ftruncate($this->fp, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the backup archive file for output. Returns false if the archive file cannot be opened in binary append
|
||||
* mode.
|
||||
*
|
||||
* @param bool $force Should I forcibly reopen the file? If false, I'll only open the file if the current
|
||||
* file pointer is null.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function openArchiveForOutput($force = false)
|
||||
{
|
||||
if (is_null($this->fp) || $force)
|
||||
{
|
||||
$this->fp = $this->fopen($this->_dataFileName, "a");
|
||||
}
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
$this->fp = null;
|
||||
|
||||
throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human formatted size to integer representation of bytes,
|
||||
* e.g. 1M to 1024768
|
||||
*
|
||||
* @param string $setting The value in human readable format, e.g. "1M"
|
||||
*
|
||||
* @return integer The value in bytes
|
||||
*/
|
||||
protected function humanToIntegerBytes($setting)
|
||||
{
|
||||
$val = trim($setting);
|
||||
$last = strtolower($val[strlen($val) - 1]);
|
||||
|
||||
if (is_numeric($last))
|
||||
{
|
||||
return $setting;
|
||||
}
|
||||
|
||||
switch ($last)
|
||||
{
|
||||
case 't':
|
||||
$val *= 1024;
|
||||
case 'g':
|
||||
$val *= 1024;
|
||||
case 'm':
|
||||
$val *= 1024;
|
||||
case 'k':
|
||||
$val *= 1024;
|
||||
}
|
||||
|
||||
return (int) $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PHP memory limit in bytes
|
||||
*
|
||||
* @return int|null Memory limit in bytes or null if we can't figure it out.
|
||||
*/
|
||||
protected function getMemoryLimit()
|
||||
{
|
||||
if (!function_exists('ini_get'))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$memLimit = ini_get("memory_limit");
|
||||
|
||||
if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
|
||||
{
|
||||
$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
|
||||
}
|
||||
|
||||
$memLimit = $this->humanToIntegerBytes($memLimit);
|
||||
|
||||
return $memLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable storing of symlink target if we are not on Windows
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function enableSymlinkTargetStorage()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$dereferenceSymlinks = $configuration->get('engine.archiver.common.dereference_symlinks', true);
|
||||
|
||||
if ($dereferenceSymlinks)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We are told not to dereference symlinks. Are we on Windows?
|
||||
$isWindows = (DIRECTORY_SEPARATOR == '\\');
|
||||
|
||||
if (function_exists('php_uname'))
|
||||
{
|
||||
$isWindows = stristr(php_uname(), 'windows');
|
||||
}
|
||||
|
||||
// If we are not on Windows, enable symlink target storage
|
||||
$this->storeSymlinkTarget = !$isWindows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file size and last modification time (also works on virtual files and symlinks)
|
||||
*
|
||||
* @param string $sourceNameOrData File path to the source file or source data (if $isVirtual is true)
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getFileSizeAndModificationTime(&$sourceNameOrData, $isVirtual, $isSymlink, $isDir)
|
||||
{
|
||||
// Get real size before compression
|
||||
if ($isVirtual)
|
||||
{
|
||||
$fileSize = akstrlen($sourceNameOrData);
|
||||
$fileModTime = time();
|
||||
|
||||
return [$fileSize, $fileModTime];
|
||||
}
|
||||
|
||||
|
||||
if ($isSymlink)
|
||||
{
|
||||
$fileSize = akstrlen(@readlink($sourceNameOrData));
|
||||
$fileModTime = 0;
|
||||
|
||||
return [$fileSize, $fileModTime];
|
||||
}
|
||||
|
||||
// Is the file readable?
|
||||
if (!is_readable($sourceNameOrData) && !$isDir)
|
||||
{
|
||||
// Really, REALLY check if it is readable (PHP sometimes lies, dammit!)
|
||||
$myFP = @$this->fopen($sourceNameOrData, 'r');
|
||||
|
||||
if ($myFP === false)
|
||||
{
|
||||
// Unreadable file, skip it.
|
||||
throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions');
|
||||
}
|
||||
|
||||
@$this->fclose($myFP);
|
||||
}
|
||||
|
||||
// Get the file size
|
||||
$fileSize = $isDir ? 0 : @filesize($sourceNameOrData);
|
||||
$fileModTime = $isDir ? 0 : @filemtime($sourceNameOrData);
|
||||
|
||||
return [$fileSize, $fileModTime];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred compression method for a file
|
||||
*
|
||||
* @param int $fileSize File size in bytes
|
||||
* @param int $memLimit Memory limit in bytes
|
||||
* @param bool $isDir Is it a directory?
|
||||
* @param bool $isSymlink Is it a symlink?
|
||||
*
|
||||
* @return int Compression method to use: 0 (uncompressed) or 1 (gzip deflate)
|
||||
*/
|
||||
protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
|
||||
{
|
||||
// If we don't have gzip installed we can't compress anything
|
||||
if (!function_exists("gzcompress"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Don't compress directories or symlinks
|
||||
if ($isDir || $isSymlink)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Do not compress files over the compression threshold
|
||||
if ($fileSize >= _AKEEBA_COMPRESSION_THRESHOLD)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// No memory limit, file smaller than the compression threshold: always compress.
|
||||
if (is_numeric($memLimit) && ($memLimit == 0))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Non-zero memory limit, PHP can report memory usage, see if there's enough memory.
|
||||
if (is_numeric($memLimit) && function_exists("memory_get_usage"))
|
||||
{
|
||||
$availableRAM = $memLimit - memory_get_usage();
|
||||
// Conservative approach: if the file size is over 40% of the available memory we won't compress.
|
||||
$compressionMethod = (($availableRAM / 2.5) >= $fileSize) ? 1 : 0;
|
||||
|
||||
return $compressionMethod;
|
||||
}
|
||||
|
||||
// Non-zero memory limit, PHP can't report memory usage, compress only files up to 512Kb (very conservative)
|
||||
return ($fileSize <= 524288) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists and is readable
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws WarningException
|
||||
*/
|
||||
protected function testIfFileExists(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
|
||||
{
|
||||
if ($isVirtual || $isDir)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!@file_exists($sourceNameOrData))
|
||||
{
|
||||
if ($isSymlink)
|
||||
{
|
||||
throw new WarningException('The symlink ' . $sourceNameOrData . ' points to a file or folder that no longer exists and will NOT be backed up.');
|
||||
}
|
||||
|
||||
throw new WarningException('The file ' . $sourceNameOrData . ' no longer exists and will NOT be backed up. Are you backing up temporary or cache data?');
|
||||
}
|
||||
|
||||
if (!@is_readable($sourceNameOrData))
|
||||
{
|
||||
throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get the compressed data for a file
|
||||
*
|
||||
* @param string $sourceNameOrData
|
||||
* @param bool $isVirtual
|
||||
* @param int $compressionMethod
|
||||
* @param string $zdata
|
||||
* @param int $unc_len
|
||||
* @param int $c_len
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function getZData(&$sourceNameOrData, &$isVirtual, &$compressionMethod, &$zdata, &$unc_len, &$c_len)
|
||||
{
|
||||
// Get uncompressed data
|
||||
$udata =& $sourceNameOrData;
|
||||
|
||||
if (!$isVirtual)
|
||||
{
|
||||
$udata = @file_get_contents($sourceNameOrData);
|
||||
}
|
||||
|
||||
// If the compression fails, we will let it behave like no compression was available
|
||||
$c_len = $unc_len;
|
||||
$compressionMethod = 0;
|
||||
|
||||
// Proceed with compression
|
||||
$zdata = @gzcompress($udata);
|
||||
|
||||
if ($zdata !== false)
|
||||
{
|
||||
// The compression succeeded
|
||||
unset($udata);
|
||||
$compressionMethod = 1;
|
||||
$zdata = aksubstr($zdata, 2, -4);
|
||||
$c_len = akstrlen($zdata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bytes available for writing data to the current part file (i.e. part size minus current offset)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getPartFreeSize()
|
||||
{
|
||||
clearstatcache();
|
||||
$current_part_size = @filesize($this->_dataFileName);
|
||||
|
||||
return (int) $this->partSize - ($current_part_size === false ? 0 : $current_part_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable split archive creation where possible
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function enableSplitArchives()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$partSize = $configuration->get('engine.archiver.common.part_size', 0);
|
||||
|
||||
// If the part size is less than 64Kb we won't enable split archives
|
||||
if ($partSize < 65536)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$extension = $this->getExtension();
|
||||
$altExtension = substr($extension, 0, 2) . '01';
|
||||
$archiveTypeUppercase = strtoupper(substr($extension, 1));
|
||||
|
||||
Factory::getLog()->info(__CLASS__ . " :: Split $archiveTypeUppercase creation enabled");
|
||||
|
||||
$this->useSplitArchive = true;
|
||||
$this->partSize = $partSize;
|
||||
$this->dataFileNameWithoutExtension =
|
||||
dirname($this->_dataFileName) . '/' . basename($this->_dataFileName, $extension);
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . $altExtension;
|
||||
|
||||
// Indicate that we have at least 1 part
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->updateMultipart(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file's GZip compressed data to the archive, taking into account archive splitting
|
||||
*
|
||||
* @param string $zdata The compressed data to write to the archive
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function putRawDataIntoArchive(&$zdata)
|
||||
{
|
||||
// Single part archive. Just dump the compressed data.
|
||||
if (!$this->useSplitArchive)
|
||||
{
|
||||
$this->fwrite($this->fp, $zdata);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Split JPA. Check if we need to split the part in the middle of the data.
|
||||
$freeSpaceInPart = $this->getPartFreeSize();
|
||||
|
||||
// Nope. We have enough space to write all of the data in this part.
|
||||
if ($freeSpaceInPart >= akstrlen($zdata))
|
||||
{
|
||||
$this->fwrite($this->fp, $zdata);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$bytesLeftInData = akstrlen($zdata);
|
||||
|
||||
while ($bytesLeftInData > 0)
|
||||
{
|
||||
// Try to write to the archive. We can only write as much bytes as the free space in the backup archive OR
|
||||
// the total data bytes left, whichever is lower.
|
||||
$bytesWritten = $this->fwrite($this->fp, $zdata, min($bytesLeftInData, $freeSpaceInPart));
|
||||
|
||||
// Since we may have written fewer bytes than anticipated we use the real bytes written for calculations
|
||||
$freeSpaceInPart -= $bytesWritten;
|
||||
$bytesLeftInData -= $bytesWritten;
|
||||
|
||||
// If we still have data to write, remove the part already written and keep the rest
|
||||
if ($bytesLeftInData > 0)
|
||||
{
|
||||
$zdata = aksubstr($zdata, -$bytesLeftInData);
|
||||
}
|
||||
|
||||
// If the part file is full create a new one
|
||||
if ($freeSpaceInPart <= 0)
|
||||
{
|
||||
// Create new part
|
||||
$this->createAndOpenNewPart();
|
||||
|
||||
// Get its free space
|
||||
$freeSpaceInPart = $this->getPartFreeSize();
|
||||
}
|
||||
}
|
||||
|
||||
// Tell PHP to free up some memory
|
||||
$zdata = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin or resume adding an uncompressed file into the archive.
|
||||
*
|
||||
* IMPORTANT! Only this case can be spanned across steps: uncompressed, non-virtual data
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file we are reading from.
|
||||
* @param int $fileLength The file size we are supposed to read, in bytes.
|
||||
* @param int $resumeOffset Offset in the file to resume reading from
|
||||
*
|
||||
* @return bool True to indicate more processing is required in the next step
|
||||
*/
|
||||
protected function putUncompressedFileIntoArchive(&$sourceNameOrData, $fileLength = 0, $resumeOffset = null)
|
||||
{
|
||||
// Copy the file contents, ignore directories
|
||||
$sourceFilePointer = @fopen($sourceNameOrData, "r");
|
||||
|
||||
if ($sourceFilePointer === false)
|
||||
{
|
||||
// If we have already written the file header and can't read the data your archive is busted.
|
||||
throw new ErrorException('Unreadable file ' . $sourceNameOrData . '. Check permissions. Your archive is corrupt!');
|
||||
}
|
||||
|
||||
// Seek to the resume point if required
|
||||
if (!is_null($resumeOffset))
|
||||
{
|
||||
// Seek to new offset
|
||||
$seek_result = @fseek($sourceFilePointer, $resumeOffset);
|
||||
|
||||
if ($seek_result === -1)
|
||||
{
|
||||
// What?! We can't resume!
|
||||
$this->conditionalFileClose($sourceFilePointer);
|
||||
|
||||
throw new ErrorException(sprintf('Could not resume packing of file %s. Your archive is damaged!', $sourceNameOrData));
|
||||
}
|
||||
|
||||
// Change the uncompressed size to reflect the remaining data
|
||||
$fileLength -= $resumeOffset;
|
||||
}
|
||||
|
||||
$mustBreak = $this->putDataFromFileIntoArchive($sourceFilePointer, $fileLength);
|
||||
|
||||
$this->conditionalFileClose($sourceFilePointer);
|
||||
|
||||
return $mustBreak;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the requested permissions for the backup archive file.
|
||||
*
|
||||
* @return int
|
||||
* @since 8.0.0
|
||||
*/
|
||||
protected function getPermissions(): int
|
||||
{
|
||||
if (!is_null($this->permissions))
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
$permissions = $configuration->get('engine.archiver.common.permissions', '0666') ?: '0666';
|
||||
$this->permissions = octdec($permissions);
|
||||
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put up to $fileLength bytes of the file pointer $sourceFilePointer into the backup archive. Returns true if we
|
||||
* ran out of time and need to perform a step break. Returns false when the whole quantity of data has been copied.
|
||||
* Throws an ErrorException if soemthing really bad happens.
|
||||
*
|
||||
* @param resource $sourceFilePointer The pointer to the input file
|
||||
* @param int $fileLength How many bytes to copy
|
||||
*
|
||||
* @return bool True to indicate we need to resume packing the file in the next step
|
||||
*/
|
||||
private function putDataFromFileIntoArchive(&$sourceFilePointer, &$fileLength)
|
||||
{
|
||||
// Get references to engine objects we're going to be using
|
||||
$configuration = Factory::getConfiguration();
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Quick copy data into the archive, AKEEBA_CHUNK bytes at a time
|
||||
while (!feof($sourceFilePointer) && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
|
||||
{
|
||||
// Normally I read up to AKEEBA_CHUNK bytes at a time
|
||||
$chunkSize = AKEEBA_CHUNK;
|
||||
|
||||
// Do I have a split ZIP?
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// I must only read up to the free space in the part file if it's less than AKEEBA_CHUNK.
|
||||
$free_space = $this->getPartFreeSize();
|
||||
$chunkSize = min($free_space, AKEEBA_CHUNK);
|
||||
|
||||
// If I ran out of free space I have to create a new part file.
|
||||
if ($free_space <= 0)
|
||||
{
|
||||
$this->createAndOpenNewPart();
|
||||
|
||||
// We have created the part. If the user asked for immediate post-proc, break step now.
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0))
|
||||
{
|
||||
$resumeOffset = @ftell($sourceFilePointer);
|
||||
$this->conditionalFileClose($sourceFilePointer);
|
||||
|
||||
$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
|
||||
$configuration->set('volatile.engine.archiver.processingfile', true);
|
||||
$configuration->set('volatile.breakflag', true);
|
||||
|
||||
// Always close the open part when immediate post-processing is requested
|
||||
@$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// No immediate post-proc. Recalculate the optimal chunk size.
|
||||
$free_space = $this->getPartFreeSize();
|
||||
$chunkSize = min($free_space, AKEEBA_CHUNK);
|
||||
}
|
||||
}
|
||||
|
||||
// Read some data and write it to the backup archive part file
|
||||
$data = fread($sourceFilePointer, $chunkSize);
|
||||
$bytesWritten = $this->fwrite($this->fp, $data, akstrlen($data));
|
||||
|
||||
// Subtract the written bytes from the bytes left to write
|
||||
$fileLength -= $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* According to the file size we read when we were writing the file header we have more data to write. However,
|
||||
* we reached the end of the file. This means the file went away or shrunk. We can't reliably go back and
|
||||
* change the file header since it may be in a previous part file that's already been post-processed. All we can
|
||||
* do is try to warn the user.
|
||||
*/
|
||||
while (feof($sourceFilePointer) && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
|
||||
{
|
||||
throw new ErrorException('The file shrunk or went away while putting it in the backup archive. Your archive is damaged! If this is a temporary or cache file we advise you to exclude the contents of the temporary / cache folder it is contained in.');
|
||||
}
|
||||
|
||||
// WARNING!!! The extra $unc_len != 0 check is necessary as PHP won't reach EOF for 0-byte files.
|
||||
if (!feof($sourceFilePointer) && ($fileLength != 0))
|
||||
{
|
||||
// We have to break, or we'll time out!
|
||||
$resumeOffset = @ftell($sourceFilePointer);
|
||||
$this->conditionalFileClose($sourceFilePointer);
|
||||
|
||||
$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
|
||||
$configuration->set('volatile.engine.archiver.processingfile', true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
if (!function_exists('akstrlen'))
|
||||
{
|
||||
/**
|
||||
* Attempt to use mbstring for calculating the binary string length.
|
||||
*
|
||||
* @param $string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function akstrlen($string)
|
||||
{
|
||||
return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for an archiver using managed file pointers
|
||||
*/
|
||||
abstract class BaseFileManagement extends Base
|
||||
{
|
||||
/** @var resource File pointer to the archive's central directory file (for ZIP) */
|
||||
protected $cdfp = null;
|
||||
|
||||
/** @var resource File pointer to the archive being currently written to */
|
||||
protected $fp = null;
|
||||
|
||||
/** @var array An array of the last open files for writing and their last written to offsets */
|
||||
private $fileOffsets = [];
|
||||
|
||||
/** @var array An array of open file pointers */
|
||||
private $filePointers = [];
|
||||
|
||||
/** @var null|string The last filename fwrite() wrote to */
|
||||
private $lastFileName = null;
|
||||
|
||||
/** @var null|resource The last file pointer fwrite() wrote to */
|
||||
private $lastFilePointer = null;
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being destroyed
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being serialized
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function _onSerialize()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all open files known to this archiver object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _closeAllFiles()
|
||||
{
|
||||
if (!empty($this->filePointers))
|
||||
{
|
||||
foreach ($this->filePointers as $file => $fp)
|
||||
{
|
||||
$this->conditionalFileClose($fp);
|
||||
|
||||
unset($this->filePointers[$file]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes an already open file
|
||||
*
|
||||
* @param resource $fp The file pointer to close
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function fclose(&$fp)
|
||||
{
|
||||
$result = true;
|
||||
|
||||
$offset = array_search($fp, $this->filePointers, true);
|
||||
|
||||
if (!is_null($fp) && is_resource($fp))
|
||||
{
|
||||
$result = $this->conditionalFileClose($fp);
|
||||
}
|
||||
|
||||
if ($offset !== false)
|
||||
{
|
||||
unset($this->filePointers[$offset]);
|
||||
}
|
||||
|
||||
$fp = null;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function fcloseByName($file)
|
||||
{
|
||||
if (!array_key_exists($file, $this->filePointers))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$ret = $this->fclose($this->filePointers[$file]);
|
||||
|
||||
if (array_key_exists($file, $this->filePointers))
|
||||
{
|
||||
unset($this->filePointers[$file]);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file, if it's not already open, or returns its cached file pointer if it's already open
|
||||
*
|
||||
* @param string $file The filename to open
|
||||
* @param string $mode File open mode, defaults to binary write
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
protected function fopen($file, $mode = 'w')
|
||||
{
|
||||
if (!array_key_exists($file, $this->filePointers))
|
||||
{
|
||||
//Factory::getLog()->debug("Opening backup archive $file with mode $mode");
|
||||
$this->filePointers[$file] = @fopen($file, $mode);
|
||||
|
||||
// If we open a file for append we have to seek to the correct offset
|
||||
if (substr($mode, 0, 1) == 'a')
|
||||
{
|
||||
if (isset($this->fileOffsets[$file]))
|
||||
{
|
||||
Factory::getLog()->debug("Truncating backup archive file $file to " . $this->fileOffsets[$file] . " bytes");
|
||||
@ftruncate($this->filePointers[$file], $this->fileOffsets[$file]);
|
||||
}
|
||||
|
||||
fseek($this->filePointers[$file], 0, SEEK_END);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filePointers[$file];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to file, defeating magic_quotes_runtime settings (pure binary write)
|
||||
*
|
||||
* @param resource $fp Handle to a file
|
||||
* @param string $data The data to write to the file
|
||||
* @param integer $p_len Maximum length of data to write
|
||||
*
|
||||
* @return int The number of bytes written
|
||||
*
|
||||
* @throws ErrorException When writing to the file is not possible
|
||||
*/
|
||||
protected function fwrite($fp, $data, $p_len = null)
|
||||
{
|
||||
if ($fp !== $this->lastFilePointer)
|
||||
{
|
||||
$this->lastFilePointer = $fp;
|
||||
$this->lastFileName = array_search($fp, $this->filePointers, true);
|
||||
}
|
||||
|
||||
$len = is_null($p_len) ? (akstrlen($data)) : $p_len;
|
||||
$ret = fwrite($fp, $data, $len);
|
||||
|
||||
if (($ret === false) || (abs(($ret - $len)) >= 1))
|
||||
{
|
||||
// Log debug information about the archive file's existence and current size. This helps us figure out if
|
||||
// there is a server-imposed maximum file size limit.
|
||||
clearstatcache();
|
||||
$fileExists = @file_exists($this->lastFileName) ? 'exists' : 'does NOT exist';
|
||||
$currentSize = @filesize($this->lastFileName);
|
||||
|
||||
Factory::getLog()->debug(sprintf("%s::_fwrite() ERROR!! Cannot write to archive file %s. The file %s. File size %s bytes after writing %s of %d bytes. Please check the output directory permissions and make sure you have enough disk space available. If this does not help, please set up a Part Size for Split Archives LOWER than this size and retry backing up.", __CLASS__, $this->lastFileName, $fileExists, $currentSize, $ret, $len));
|
||||
|
||||
throw new ErrorException(sprintf("Couldn\'t write to the archive file; check the output directory permissions and make sure you have enough disk space available. [len=%s / %s]", $ret, $len));
|
||||
}
|
||||
|
||||
if ($this->lastFileName !== false)
|
||||
{
|
||||
$this->fileOffsets[$this->lastFileName] = @ftell($fp);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a file path from the list of resumable offsets
|
||||
*
|
||||
* @param $filename
|
||||
*/
|
||||
protected function removeFromOffsetsList($filename)
|
||||
{
|
||||
if (isset($this->fileOffsets[$filename]))
|
||||
{
|
||||
unset($this->fileOffsets[$filename]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* JPA creation class
|
||||
*
|
||||
* JPA Format 1.2 implemented, minus BZip2 compression support
|
||||
*/
|
||||
class Jpa extends BaseArchiver
|
||||
{
|
||||
/** @var integer How many files are contained in the archive */
|
||||
private $totalFilesCount = 0;
|
||||
|
||||
/** @var integer The total size of files contained in the archive as they are stored */
|
||||
private $totalCompressedSize = 0;
|
||||
|
||||
/** @var integer The total size of files contained in the archive when they are extracted to disk. */
|
||||
private $totalUncompressedSize = 0;
|
||||
|
||||
/** @var string Standard Header signature */
|
||||
private $archiveSignature = "\x4A\x50\x41";
|
||||
|
||||
/** @var string Entity Block signature */
|
||||
private $fileHeaderSignature = "\x4A\x50\x46";
|
||||
|
||||
/** @var string Marks the split archive's extra header */
|
||||
private $splitArchiveExtraHeader = "\x4A\x50\x01\x01"; //
|
||||
|
||||
/** @var int Current part file number */
|
||||
private $currentPartNumber = 1;
|
||||
|
||||
/** @var int Total number of part files */
|
||||
private $totalParts = 1;
|
||||
|
||||
/**
|
||||
* Initialises the archiver class, creating the archive from an existent
|
||||
* installer's JPA archive.
|
||||
*
|
||||
* @param string $targetArchivePath Absolute path to the generated archive
|
||||
* @param array $options A named key array of options (optional)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize($targetArchivePath, $options = [])
|
||||
{
|
||||
Factory::getLog()->debug(__CLASS__ . " :: new instance - archive $targetArchivePath");
|
||||
$this->_dataFileName = $targetArchivePath;
|
||||
|
||||
// Should we enable Split ZIP feature?
|
||||
$this->enableSplitArchives();
|
||||
|
||||
// Should I use Symlink Target Storage?
|
||||
$this->enableSymlinkTargetStorage();
|
||||
|
||||
// Try to kill the archive if it exists
|
||||
$this->createNewBackupArchive();
|
||||
|
||||
// Write the initial instance of the archive header
|
||||
$this->_writeArchiveHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Standard Header with current information
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finalize()
|
||||
{
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (is_resource($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
$this->_closeAllFiles();
|
||||
|
||||
// If Spanned JPA and there is no .jpa file, rename the last fragment to .jpa
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$extension = substr($this->_dataFileName, -4);
|
||||
|
||||
if ($extension != '.jpa')
|
||||
{
|
||||
Factory::getLog()->debug('Renaming last JPA part to .JPA extension');
|
||||
|
||||
$newName = $this->dataFileNameWithoutExtension . '.jpa';
|
||||
|
||||
if (!@rename($this->_dataFileName, $newName))
|
||||
{
|
||||
throw new RuntimeException('Could not rename last JPA part to .JPA extension.');
|
||||
}
|
||||
|
||||
$this->_dataFileName = $newName;
|
||||
}
|
||||
|
||||
// Finally, point to the first part so that we can re-write the correct header information
|
||||
if ($this->totalParts > 1)
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j01';
|
||||
}
|
||||
}
|
||||
|
||||
// Re-write the archive header
|
||||
$this->_writeArchiveHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the extension (including the dot) of the files produced
|
||||
* by this class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExtension()
|
||||
{
|
||||
return '.jpa';
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a Standard Header at the top of the file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _writeArchiveHeader()
|
||||
{
|
||||
if (!is_null($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
}
|
||||
|
||||
$this->fp = $this->fopen($this->_dataFileName, 'c');
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
throw new ErrorException('Could not open ' . $this->_dataFileName . ' for writing. Check permissions and open_basedir restrictions.');
|
||||
}
|
||||
|
||||
// Calculate total header size
|
||||
$headerSize = 19; // Standard Header
|
||||
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Spanned JPA header
|
||||
$headerSize += 8;
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, $this->archiveSignature); // ID string (JPA)
|
||||
$this->fwrite($this->fp, pack('v', $headerSize)); // Header length; fixed to 19 bytes
|
||||
$this->fwrite($this->fp, pack('C', _JPA_MAJOR)); // Major version
|
||||
$this->fwrite($this->fp, pack('C', _JPA_MINOR)); // Minor version
|
||||
$this->fwrite($this->fp, pack('V', $this->totalFilesCount)); // File count
|
||||
$this->fwrite($this->fp, pack('V', $this->totalUncompressedSize)); // Size of files when extracted
|
||||
$this->fwrite($this->fp, pack('V', $this->totalCompressedSize)); // Size of files when stored
|
||||
|
||||
// Do I need to add a split archive's header too?
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$this->fwrite($this->fp, $this->splitArchiveExtraHeader); // Signature
|
||||
$this->fwrite($this->fp, pack('v', 4)); // Extra field length
|
||||
$this->fwrite($this->fp, pack('v', $this->totalParts)); // Number of parts
|
||||
}
|
||||
|
||||
$this->fclose($this->fp);
|
||||
|
||||
@chmod($this->_dataFileName, $this->getPermissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the bootstrap code to add some define's used by the JPA format engine
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __bootstrap_code()
|
||||
{
|
||||
if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
|
||||
|
||||
/**
|
||||
* Akeeba Backup and JPA Format version change chart:
|
||||
* Akeeba Backup 3.0: JPA Format 1.1 is used
|
||||
* Akeeba Backup 3.1: JPA Format 1.2 with file modification timestamp is used
|
||||
*/
|
||||
define('_JPA_MAJOR', 1); // JPA Format major version number
|
||||
define('_JPA_MINOR', 2); // JPA Format minor version number
|
||||
|
||||
}
|
||||
parent::__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* The most basic file transaction: add a single entry (file or directory) to
|
||||
* the archive.
|
||||
*
|
||||
* @param bool $isVirtual If true, the next parameter contains file data instead of a file name
|
||||
* @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual is
|
||||
* true
|
||||
* @param string $targetName The (relative) file name under which to store the file in the archive
|
||||
*
|
||||
* @return boolean True on success, false otherwise
|
||||
*
|
||||
* @since 1.2.1
|
||||
*/
|
||||
protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
|
||||
{
|
||||
// Get references to engine objects we're going to be using
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Is this a virtual file?
|
||||
$isVirtual = (bool) $isVirtual;
|
||||
|
||||
// Open data file for output
|
||||
$this->openArchiveForOutput();
|
||||
|
||||
// Should I continue backing up a file from the previous step?
|
||||
$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
|
||||
// definition we have an uncompressed, non-virtual file. Hence the default values.
|
||||
$isDir = false;
|
||||
$isSymlink = false;
|
||||
$compressionMethod = 0;
|
||||
$zdata = null;
|
||||
// If we are continuing file packing we have an uncompressed, non-virtual file.
|
||||
$isVirtual = $continueProcessingFile ? false : $isVirtual;
|
||||
$resume = $continueProcessingFile ? 0 : null;
|
||||
|
||||
if (!$continueProcessingFile)
|
||||
{
|
||||
// Log the file being added
|
||||
$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
|
||||
Factory::getLog()->debug("-- Adding $targetName to archive $messageSource");
|
||||
|
||||
// Write a file header
|
||||
$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir, $compressionMethod, $zdata, $unc_len);
|
||||
}
|
||||
else
|
||||
{
|
||||
$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
|
||||
$unc_len = $configuration->get('volatile.engine.archiver.unc_len', 0);
|
||||
$resume = $configuration->get('volatile.engine.archiver.resume', 0);
|
||||
|
||||
// Log the file we continue packing
|
||||
Factory::getLog()->debug("-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
|
||||
}
|
||||
|
||||
/* "File data" segment. */
|
||||
if ($compressionMethod == 1)
|
||||
{
|
||||
// Compressed data. Put into the archive.
|
||||
$this->putRawDataIntoArchive($zdata);
|
||||
}
|
||||
elseif ($isVirtual)
|
||||
{
|
||||
// Virtual data. Put into the archive.
|
||||
$this->putRawDataIntoArchive($sourceNameOrData);
|
||||
}
|
||||
elseif ($isSymlink)
|
||||
{
|
||||
// Symlink. Just put the link target into the archive.
|
||||
$this->fwrite($this->fp, @readlink($sourceNameOrData));
|
||||
}
|
||||
elseif ((!$isDir) && (!$isSymlink))
|
||||
{
|
||||
// Uncompressed file.
|
||||
if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
|
||||
{
|
||||
// If it returns true we are doing a step break to resume packing in the next step. So we need to return
|
||||
// true here to avoid running the final bit of code which uncaches the file resume data.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Factory::getLog()->debug("DEBUG -- Added $targetName to archive");
|
||||
|
||||
// Uncache data
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
$configuration->set('volatile.engine.archiver.unc_len', null);
|
||||
$configuration->set('volatile.engine.archiver.resume', null);
|
||||
$configuration->set('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// ... and return TRUE = success
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the file header to the backup archive.
|
||||
*
|
||||
* Only the first three parameters are input. All other are ignored for input and are overwritten.
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param string $targetName The target path to be stored inside the archive
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
* @param int $compressionMethod The compression method chosen for this file
|
||||
* @param string $zdata If we have compression method other than 0 this holds the compressed data.
|
||||
* We return that from this method to avoid having to compress the same data
|
||||
* twice (once to write the compressed data length in the header and once to
|
||||
* write the compressed data to the archive).
|
||||
* @param int $unc_len The uncompressed size of the file / source data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function writeFileHeader(&$sourceNameOrData, &$targetName, &$isVirtual, &$isSymlink, &$isDir, &$compressionMethod, &$zdata, &$unc_len)
|
||||
{
|
||||
static $memLimit = null;
|
||||
|
||||
if (is_null($memLimit))
|
||||
{
|
||||
$memLimit = $this->getMemoryLimit();
|
||||
}
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Uncache data -- WHY DO THAT?!
|
||||
/**
|
||||
* $configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
* $configuration->set('volatile.engine.archiver.unc_len', null);
|
||||
* $configuration->set('volatile.engine.archiver.resume', null);
|
||||
* $configuration->set('volatile.engine.archiver.processingfile',false);
|
||||
* /**/
|
||||
|
||||
// See if it's a directory
|
||||
$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
|
||||
|
||||
// See if it's a symlink (w/out dereference)
|
||||
$isSymlink = false;
|
||||
|
||||
if ($this->storeSymlinkTarget && !$isVirtual)
|
||||
{
|
||||
$isSymlink = is_link($sourceNameOrData);
|
||||
}
|
||||
|
||||
// Get real size before compression
|
||||
[$fileSize, $fileModTime] =
|
||||
$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);
|
||||
|
||||
// Decide if we will compress
|
||||
$compressionMethod = $this->getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
|
||||
|
||||
$storedName = $targetName;
|
||||
|
||||
/* "Entity Description Block" segment. */
|
||||
$unc_len = $fileSize; // File size
|
||||
$storedName .= ($isDir) ? "/" : "";
|
||||
|
||||
/**
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !!!! WARNING!!! DO NOT MOVE THIS BLOCK OF CODE AFTER THE testIfFileExists OR getZData!!!! !!!!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
*
|
||||
* PHP 5.6.3 IS BROKEN. Possibly the same applies for all old versions of PHP. If you try to get the file
|
||||
* permissions after reading its contents PHP segfaults.
|
||||
*/
|
||||
// Get file permissions
|
||||
$perms = 0644;
|
||||
|
||||
if (!$isVirtual)
|
||||
{
|
||||
$perms = @fileperms($sourceNameOrData);
|
||||
}
|
||||
|
||||
// Test for non-existing or unreadable files
|
||||
$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
|
||||
|
||||
// Default compressed (archived) length = uncompressed length – valid unless we can actually compress the data.
|
||||
$c_len = $unc_len;
|
||||
|
||||
if ($compressionMethod == 1)
|
||||
{
|
||||
$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
|
||||
}
|
||||
|
||||
$this->totalCompressedSize += $c_len; // Update global data
|
||||
$this->totalUncompressedSize += $fileSize; // Update global data
|
||||
$this->totalFilesCount++;
|
||||
|
||||
// Calculate Entity Description Block length
|
||||
$blockLength = 21 + akstrlen($storedName);
|
||||
|
||||
// If we need to store the file mod date
|
||||
if ($fileModTime > 0)
|
||||
{
|
||||
$blockLength += 8;
|
||||
}
|
||||
|
||||
// Get file type
|
||||
$fileType = 1;
|
||||
|
||||
if ($isSymlink)
|
||||
{
|
||||
$fileType = 2;
|
||||
}
|
||||
elseif ($isDir)
|
||||
{
|
||||
$fileType = 0;
|
||||
}
|
||||
|
||||
// If it's a split JPA file, we've got to make sure that the header can fit in the part
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Compare to free part space
|
||||
$free_space = $this->getPartFreeSize();
|
||||
|
||||
if ($free_space <= $blockLength)
|
||||
{
|
||||
// Not enough space on current part, create new part
|
||||
$this->createAndOpenNewPart();
|
||||
}
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, $this->fileHeaderSignature); // Entity Description Block header
|
||||
$this->fwrite($this->fp, pack('v', $blockLength)); // Entity Description Block header length
|
||||
$this->fwrite($this->fp, pack('v', akstrlen($storedName))); // Length of entity path
|
||||
$this->fwrite($this->fp, $storedName); // Entity path
|
||||
$this->fwrite($this->fp, pack('C', $fileType)); // Entity type
|
||||
$this->fwrite($this->fp, pack('C', $compressionMethod)); // Compression method
|
||||
$this->fwrite($this->fp, pack('V', $c_len)); // Compressed size
|
||||
$this->fwrite($this->fp, pack('V', $unc_len)); // Uncompressed size
|
||||
$this->fwrite($this->fp, pack('V', $perms)); // Entity permissions
|
||||
|
||||
// Timestamp Extra Field, only for files
|
||||
if ($fileModTime > 0)
|
||||
{
|
||||
$this->fwrite($this->fp, "\x00\x01"); // Extra Field Identifier
|
||||
$this->fwrite($this->fp, pack('v', 8)); // Extra Field Length
|
||||
$this->fwrite($this->fp, pack('V', $fileModTime)); // Timestamp
|
||||
}
|
||||
|
||||
// Cache useful information about the file
|
||||
if (!$isDir && !$isSymlink && !$isVirtual)
|
||||
{
|
||||
$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new part for the spanned archive
|
||||
*
|
||||
* @param bool $finalPart Is this the final archive part?
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
protected function createNewPartFile($finalPart = false)
|
||||
{
|
||||
// Close any open file pointers
|
||||
if (!is_resource($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (is_resource($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
// Remove the just finished part from the list of resumable offsets
|
||||
$this->removeFromOffsetsList($this->_dataFileName);
|
||||
|
||||
// Set the file pointers to null
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
|
||||
// Push the previous part if we have to post-process it immediately
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0))
|
||||
{
|
||||
// The first part needs its header overwritten during archive
|
||||
// finalization. Skip it from immediate processing.
|
||||
if ($this->currentPartNumber != 1)
|
||||
{
|
||||
$this->finishedPart[] = $this->_dataFileName;
|
||||
}
|
||||
}
|
||||
|
||||
$this->totalParts++;
|
||||
$this->currentPartNumber = $this->totalParts;
|
||||
|
||||
if ($finalPart)
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.jpa';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j' . sprintf('%02d', $this->currentPartNumber);
|
||||
}
|
||||
|
||||
Factory::getLog()->info('Creating new JPA part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->updateMultipart($this->totalParts);
|
||||
|
||||
// Try to remove any existing file
|
||||
@unlink($this->_dataFileName);
|
||||
|
||||
// Touch the new file
|
||||
$result = @touch($this->_dataFileName);
|
||||
|
||||
chmod($this->_dataFileName, $this->getPermissions());
|
||||
|
||||
// Try to write 6 bytes to it
|
||||
if ($result)
|
||||
{
|
||||
$result = @file_put_contents($this->_dataFileName, 'AKEEBA') == 6;
|
||||
}
|
||||
|
||||
if ($result)
|
||||
{
|
||||
@unlink($this->_dataFileName);
|
||||
|
||||
$result = @touch($this->_dataFileName);
|
||||
@chmod($this->_dataFileName, $this->getPermissions());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,971 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Base\Exceptions\WarningException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Util\CRC32;
|
||||
use RuntimeException;
|
||||
|
||||
class Zip extends BaseArchiver
|
||||
{
|
||||
/** @var string Beginning of central directory record. */
|
||||
private $centralDirectoryRecordStartSignature = "\x50\x4b\x01\x02";
|
||||
|
||||
/** @var string End of central directory record. */
|
||||
private $centralDirectoryRecordEndSignature = "\x50\x4b\x05\x06";
|
||||
|
||||
/** @var string Beginning of file contents. */
|
||||
private $fileHeaderSignature = "\x50\x4b\x03\x04";
|
||||
|
||||
/** @var string The name of the temporary file holding the ZIP's Central Directory */
|
||||
private $centralDirectoryFilename;
|
||||
|
||||
/** @var integer The total number of files and directories stored in the ZIP archive */
|
||||
private $totalFilesCount;
|
||||
|
||||
/** @var integer The total size of data in the archive. Note: On 32-bit versions of PHP, this will overflow for archives over 2Gb! */
|
||||
private $totalCompressedSize = 0;
|
||||
|
||||
/** @var integer The chunk size for CRC32 calculations */
|
||||
private $AkeebaPackerZIP_CHUNK_SIZE;
|
||||
|
||||
/** @var int Current part file number */
|
||||
private $currentPartNumber = 1;
|
||||
|
||||
/** @var int Total number of part files */
|
||||
private $totalParts = 1;
|
||||
|
||||
/**
|
||||
* Class constructor - initializes internal operating parameters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->debug(__CLASS__ . " :: New instance");
|
||||
|
||||
// Find the optimal chunk size for ZIP archive processing
|
||||
$this->findOptimalChunkSize();
|
||||
|
||||
Factory::getLog()->debug("Chunk size for CRC is now " . $this->AkeebaPackerZIP_CHUNK_SIZE . " bytes");
|
||||
|
||||
// Should I use Symlink Target Storage?
|
||||
$this->enableSymlinkTargetStorage();
|
||||
|
||||
parent::__construct();
|
||||
|
||||
if (!function_exists('hash_file') || !function_exists('hash'))
|
||||
{
|
||||
$action = version_compare(PHP_VERSION, '7.4.0', 'lt')
|
||||
? 'Please ask your host to enable the PHP hash extension and to make sure the hash_file() and hash() functions are not disabled.'
|
||||
: 'Please ask your host to change their PHP configuration so that the hash_file() and hash() functions are not disabled.';
|
||||
|
||||
Factory::getLog()->warning(
|
||||
sprintf(
|
||||
'Your server lacks support for the hash_file() and/or hash() functions. CRC32 checksum cannot be calculated. Third party ZIP extraction tools may report the backup archive as broken. %s',
|
||||
$action
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the archiver class, creating the archive from an existent
|
||||
* installer's JPA archive.
|
||||
*
|
||||
* @param string $sourceJPAPath Absolute path to an installer's JPA archive
|
||||
* @param string $targetArchivePath Absolute path to the generated archive
|
||||
* @param array $options A named key array of options (optional). This is currently not supported
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize($targetArchivePath, $options = [])
|
||||
{
|
||||
Factory::getLog()->debug(__CLASS__ . " :: initialize - archive $targetArchivePath");
|
||||
|
||||
// Get names of temporary files
|
||||
$this->_dataFileName = $targetArchivePath;
|
||||
|
||||
// Should we enable split archive feature?
|
||||
$this->enableSplitArchives();
|
||||
|
||||
// Create the Central Directory temporary file
|
||||
$this->createCentralDirectoryTempFile();
|
||||
|
||||
// Try to kill the archive if it exists
|
||||
$this->createNewBackupArchive();
|
||||
|
||||
// On split archives, include the "Split ZIP" header, for PKZIP 2.50+ compatibility
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$this->openArchiveForOutput();
|
||||
$this->fwrite($this->fp, "\x50\x4b\x07\x08");
|
||||
}
|
||||
}
|
||||
|
||||
public function finalize()
|
||||
{
|
||||
$this->finalizeZIPFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Glues the Central Directory of the ZIP file to the archive and takes care about the differences between single
|
||||
* and multipart archives.
|
||||
*
|
||||
* Official ZIP file format: http://www.pkware.com/appnote.txt
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finalizeZIPFile()
|
||||
{
|
||||
// 1. Get size of central directory
|
||||
clearstatcache();
|
||||
$cdOffset = @filesize($this->_dataFileName);
|
||||
$this->totalCompressedSize += $cdOffset;
|
||||
$cdSize = @filesize($this->centralDirectoryFilename);
|
||||
|
||||
// 2. Append Central Directory to data file and remove the CD temp file afterwards
|
||||
if (!is_null($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (!is_null($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
$this->openArchiveForOutput(true);
|
||||
|
||||
/**
|
||||
* Do not remove the fcloseByName line! This is required for post-processing multipart archives when for any
|
||||
* reason $this->filePointers[$this->centralDirectoryFilename] contains null instead of boolean false. In this
|
||||
* case the while loop would be stuck forever and the backup would fail. This HAS happened and I have been able
|
||||
* to reproduce it but I did not have enough time to identify the real root cause. This workaround, however,
|
||||
* works.
|
||||
*/
|
||||
$this->fcloseByName($this->centralDirectoryFilename);
|
||||
$this->cdfp = $this->fopen($this->centralDirectoryFilename, "r");
|
||||
|
||||
if ($this->cdfp === false)
|
||||
{
|
||||
// Already glued, return
|
||||
$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Comment length (I need it before I start gluing the archive)
|
||||
$comment_length = akstrlen($this->_comment);
|
||||
|
||||
// Special consideration for split ZIP files
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Calculate size of Central Directory + EOCD records
|
||||
$total_cd_eocd_size = $cdSize + 22 + $comment_length;
|
||||
|
||||
// Free space on the part
|
||||
$free_space = $this->getPartFreeSize();
|
||||
|
||||
if (($free_space < $total_cd_eocd_size) && ($total_cd_eocd_size > 65536))
|
||||
{
|
||||
// Not enough space on archive for CD + EOCD, will go on separate part
|
||||
$this->createAndOpenNewPart(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the CD record
|
||||
*
|
||||
* Note about is_resource: in some circumstances where multipart ZIP files are generated, the $this->cdfp will
|
||||
* contain a null value. This seems to happen when $this->fopen returns null, i.e. $this->filePointers has a
|
||||
* null value instead of a file pointer (resource). Why this happens is unclear but the workaround is to remove
|
||||
* the null value from $this->filePointers and retry $this->fopen. Normally this should not be required since we
|
||||
* already to the fcloseByName/fopen dance above. This if-block is our last hope to catch a potential issue
|
||||
* which would either make the while loop go infinite (not anymore, I've patched it) or the Central Directory
|
||||
* not get written to the archive, which results in a broken archive.
|
||||
*/
|
||||
if (!is_resource($this->cdfp))
|
||||
{
|
||||
$this->fcloseByName($this->centralDirectoryFilename);
|
||||
$this->cdfp = $this->fopen($this->centralDirectoryFilename, "r");
|
||||
|
||||
// We tried reopening the central directory file and failed again. Time to report a fatal error.
|
||||
if (!$this->cdfp)
|
||||
{
|
||||
throw new RuntimeException("Cannot open central directory temporary file {$this->centralDirectoryFilename} for reading.");
|
||||
}
|
||||
}
|
||||
|
||||
while (!feof($this->cdfp) && is_resource($this->cdfp))
|
||||
{
|
||||
/**
|
||||
* Why not split the Central Directory between parts?
|
||||
*
|
||||
* APPNOTE.TXT §8.5.2 "The central directory may span segment boundaries, but no single record in the
|
||||
* central directory should be split across segments."
|
||||
*
|
||||
* This would require parsing the CD temp file to prevent any CD record from spanning across two parts.
|
||||
* But how many bytes is each CD record? It's about 100 bytes per file which gives us about 10,400 files
|
||||
* per MB. Even a 2MB part size holds more than 20,000 file records. A typical 10Mb part size holds more
|
||||
* files than the largest backup I've ever seen. Therefore there is no need to waste computational power
|
||||
* to see if we need to span the Central Directory between parts.
|
||||
*/
|
||||
$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
|
||||
$this->fwrite($this->fp, $chunk);
|
||||
}
|
||||
|
||||
unset($chunk);
|
||||
|
||||
// Delete the temporary CD file
|
||||
$this->fclose($this->cdfp);
|
||||
$this->cdfp = null;
|
||||
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->centralDirectoryFilename);
|
||||
|
||||
// 3. Write the rest of headers to the end of the ZIP file
|
||||
$this->fwrite($this->fp, $this->centralDirectoryRecordEndSignature);
|
||||
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Split ZIP files, enter relevant disk number information
|
||||
$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Number of this disk. */
|
||||
$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Disk with central directory start. */
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-split ZIP files, the disk number MUST be 0
|
||||
$this->fwrite($this->fp, pack('V', 0));
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries "on this disk". */
|
||||
$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries overall. */
|
||||
$this->fwrite($this->fp, pack('V', $cdSize)); /* Size of central directory. */
|
||||
$this->fwrite($this->fp, pack('V', $cdOffset)); /* Offset to start of central dir. */
|
||||
|
||||
// 2.0.b2 -- Write a ZIP file comment
|
||||
$this->fwrite($this->fp, pack('v', $comment_length)); /* ZIP file comment length. */
|
||||
$this->fwrite($this->fp, $this->_comment);
|
||||
$this->fclose($this->fp);
|
||||
|
||||
// If Split ZIP and there is no .zip file, rename the last fragment to .ZIP
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$extension = substr($this->_dataFileName, -3);
|
||||
|
||||
if ($extension != '.zip')
|
||||
{
|
||||
Factory::getLog()->debug('Renaming last ZIP part to .ZIP extension');
|
||||
|
||||
$newName = $this->dataFileNameWithoutExtension . '.zip';
|
||||
|
||||
if (!@rename($this->_dataFileName, $newName))
|
||||
{
|
||||
throw new RuntimeException('Could not rename last ZIP part to .ZIP extension.');
|
||||
}
|
||||
|
||||
$this->_dataFileName = $newName;
|
||||
}
|
||||
|
||||
// If Split ZIP and only one fragment, change the signature
|
||||
if ($this->totalParts == 1)
|
||||
{
|
||||
$this->fp = $this->fopen($this->_dataFileName, 'r+');
|
||||
$this->fwrite($this->fp, "\x50\x4b\x30\x30");
|
||||
}
|
||||
}
|
||||
|
||||
@chmod($this->_dataFileName, $this->getPermissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the extension (including the dot) of the files produced
|
||||
* by this class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExtension()
|
||||
{
|
||||
return '.zip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the bootstrap code to add some define's used by the ZIP format engine
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __bootstrap_code()
|
||||
{
|
||||
if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
|
||||
define("_AKEEBA_DIRECTORY_READ_CHUNK", $config->get('engine.archiver.zip.cd_glue_chunk_size')); // How much data to read at once when finalizing ZIP archives
|
||||
}
|
||||
|
||||
parent::__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* The most basic file transaction: add a single entry (file or directory) to
|
||||
* the archive.
|
||||
*
|
||||
* @param bool $isVirtual If true, the next parameter contains file data instead of a file name
|
||||
* @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual is
|
||||
* true
|
||||
* @param string $targetName The (relative) file name under which to store the file in the archive
|
||||
*
|
||||
* @return bool True on success, false otherwise
|
||||
*/
|
||||
protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Note down the starting disk number for Split ZIP archives
|
||||
$starting_disk_number_for_this_file = 0;
|
||||
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$starting_disk_number_for_this_file = $this->currentPartNumber - 1;
|
||||
}
|
||||
|
||||
// Open data file for output
|
||||
$this->openArchiveForOutput();
|
||||
|
||||
// Should I continue backing up a file from the previous step?
|
||||
$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
|
||||
// definition we have an uncompressed, non-virtual file. Hence the default values.
|
||||
$isDir = false;
|
||||
$isSymlink = false;
|
||||
$compressionMethod = 1;
|
||||
$zdata = null;
|
||||
// If we are continuing file packing we have an uncompressed, non-virtual file.
|
||||
$isVirtual = $continueProcessingFile ? false : $isVirtual;
|
||||
$resume = $continueProcessingFile ? 0 : null;
|
||||
|
||||
if (!$continueProcessingFile)
|
||||
{
|
||||
// Log the file being added
|
||||
$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
|
||||
Factory::getLog()->debug("-- Adding $targetName to archive $messageSource");
|
||||
|
||||
$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir,
|
||||
$compressionMethod, $zdata, $unc_len,
|
||||
$storedName, $crc, $c_len, $hexdtime, $old_offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since we are continuing archiving, it's an uncompressed regular file. Set up the variables.
|
||||
$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
|
||||
$resume = $configuration->get('volatile.engine.archiver.resume', 0);
|
||||
$unc_len = $configuration->get('volatile.engine.archiver.unc_len');
|
||||
$storedName = $configuration->get('volatile.engine.archiver.storedName');
|
||||
$crc = $configuration->get('volatile.engine.archiver.crc');
|
||||
$c_len = $configuration->get('volatile.engine.archiver.c_len');
|
||||
$hexdtime = $configuration->get('volatile.engine.archiver.hexdtime');
|
||||
$old_offset = $configuration->get('volatile.engine.archiver.old_offset');
|
||||
|
||||
// Log the file we continue packing
|
||||
Factory::getLog()->debug("-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
|
||||
}
|
||||
|
||||
/* "File data" segment. */
|
||||
if ($compressionMethod == 8)
|
||||
{
|
||||
$this->putRawDataIntoArchive($zdata);
|
||||
}
|
||||
elseif ($isVirtual)
|
||||
{
|
||||
// Virtual data. Put into the archive.
|
||||
$this->putRawDataIntoArchive($sourceNameOrData);
|
||||
}
|
||||
elseif ($isSymlink)
|
||||
{
|
||||
$this->fwrite($this->fp, @readlink($sourceNameOrData));
|
||||
}
|
||||
elseif ((!$isDir) && (!$isSymlink))
|
||||
{
|
||||
// Uncompressed file.
|
||||
if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
|
||||
{
|
||||
// If it returns true we are doing a step break to resume packing in the next step. So we need to return
|
||||
// true here to avoid running the final bit of code which writes the central directory record and
|
||||
// uncaches the file resume data.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the central directory file for append
|
||||
if (is_null($this->cdfp))
|
||||
{
|
||||
$this->cdfp = @$this->fopen($this->centralDirectoryFilename, "a");
|
||||
}
|
||||
|
||||
if ($this->cdfp === false)
|
||||
{
|
||||
throw new ErrorException("Could not open Central Directory temporary file for append!");
|
||||
}
|
||||
|
||||
$this->fwrite($this->cdfp, $this->centralDirectoryRecordStartSignature);
|
||||
|
||||
if (!$isSymlink)
|
||||
{
|
||||
$this->fwrite($this->cdfp, "\x14\x00"); /* Version made by (always set to 2.0). */
|
||||
$this->fwrite($this->cdfp, "\x14\x00"); /* Version needed to extract */
|
||||
$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
|
||||
$this->fwrite($this->cdfp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
|
||||
}
|
||||
else
|
||||
{
|
||||
// Symlinks get special treatment
|
||||
$this->fwrite($this->cdfp, "\x14\x03"); /* Version made by (version 2.0 with UNIX extensions). */
|
||||
$this->fwrite($this->cdfp, "\x0a\x03"); /* Version needed to extract */
|
||||
$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
|
||||
$this->fwrite($this->cdfp, "\x00\x00"); /* Compression method. */
|
||||
}
|
||||
|
||||
$this->fwrite($this->cdfp, $hexdtime); /* Last mod time/date. */
|
||||
$this->fwrite($this->cdfp, $crc); /* CRC 32 information. */
|
||||
$this->fwrite($this->cdfp, pack('V', $c_len)); /* Compressed filesize. */
|
||||
|
||||
if ($compressionMethod == 0)
|
||||
{
|
||||
// When we are not compressing, $unc_len is being reduced to 0 while backing up.
|
||||
// With this trick, we always store the correct length, as in this case the compressed
|
||||
// and uncompressed length is always the same.
|
||||
$this->fwrite($this->cdfp, pack('V', $c_len)); /* Uncompressed filesize. */
|
||||
}
|
||||
else
|
||||
{
|
||||
// When compressing, the uncompressed length differs from compressed length
|
||||
// and this line writes the correct value.
|
||||
$this->fwrite($this->cdfp, pack('V', $unc_len)); /* Uncompressed filesize. */
|
||||
}
|
||||
|
||||
$fn_length = akstrlen($storedName);
|
||||
$this->fwrite($this->cdfp, pack('v', $fn_length)); /* Length of filename. */
|
||||
$this->fwrite($this->cdfp, pack('v', 0)); /* Extra field length. */
|
||||
$this->fwrite($this->cdfp, pack('v', 0)); /* File comment length. */
|
||||
$this->fwrite($this->cdfp, pack('v', $starting_disk_number_for_this_file)); /* Disk number start. */
|
||||
$this->fwrite($this->cdfp, pack('v', 0)); /* Internal file attributes. */
|
||||
|
||||
/* External file attributes */
|
||||
if (!$isSymlink)
|
||||
{
|
||||
// Archive bit set
|
||||
$this->fwrite($this->cdfp, pack('V', $isDir ? 0x41FF0010 : 0xFE49FFE0));
|
||||
}
|
||||
else
|
||||
{
|
||||
// For SymLinks we store UNIX file attributes
|
||||
$this->fwrite($this->cdfp, "\x20\x80\xFF\xA1");
|
||||
}
|
||||
|
||||
$this->fwrite($this->cdfp, pack('V', $old_offset)); /* Relative offset of local header. */
|
||||
$this->fwrite($this->cdfp, $storedName); /* File name. */
|
||||
|
||||
/* Optional extra field, file comment goes here. */
|
||||
|
||||
// Finally, increase the file counter by one
|
||||
$this->totalFilesCount++;
|
||||
|
||||
// Uncache data
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
$configuration->set('volatile.engine.archiver.unc_len', null);
|
||||
$configuration->set('volatile.engine.archiver.resume', null);
|
||||
$configuration->set('volatile.engine.archiver.hexdtime', null);
|
||||
$configuration->set('volatile.engine.archiver.crc', null);
|
||||
$configuration->set('volatile.engine.archiver.c_len', null);
|
||||
$configuration->set('volatile.engine.archiver.fn_length', null);
|
||||
$configuration->set('volatile.engine.archiver.old_offset', null);
|
||||
$configuration->set('volatile.engine.archiver.storedName', null);
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
|
||||
$configuration->set('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// ... and return TRUE = success
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the file header before putting the file data into the archive
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param string $targetName The target path to be stored inside the archive
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
* @param int $compressionMethod The compression method chosen for this file
|
||||
* @param string $zdata If we have compression method other than 0 this holds the compressed data.
|
||||
* We return that from this method to avoid having to compress the same data
|
||||
* twice (once to write the compressed data length in the header and once to
|
||||
* write the compressed data to the archive).
|
||||
* @param int $unc_len The uncompressed size of the file / source data
|
||||
*
|
||||
* @param string $storedName The file path stored in the archive
|
||||
* @param string $crc CRC-32 for the file
|
||||
* @param int $c_len Compressed data length
|
||||
* @param string $hexdtime ZIP's hexadecimal notation if the file's modification date
|
||||
* @param int $old_offset Offset of the file header in the part file
|
||||
*/
|
||||
protected function writeFileHeader(&$sourceNameOrData, $targetName, &$isVirtual, &$isSymlink, &$isDir,
|
||||
&$compressionMethod, &$zdata, &$unc_len, &$storedName, &$crc, &$c_len,
|
||||
&$hexdtime, &$old_offset)
|
||||
{
|
||||
static $memLimit = null;
|
||||
|
||||
if (is_null($memLimit))
|
||||
{
|
||||
$memLimit = $this->getMemoryLimit();
|
||||
}
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// See if it's a directory
|
||||
$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
|
||||
|
||||
// See if it's a symlink (w/out dereference)
|
||||
$isSymlink = false;
|
||||
|
||||
if ($this->storeSymlinkTarget && !$isVirtual)
|
||||
{
|
||||
$isSymlink = is_link($sourceNameOrData);
|
||||
}
|
||||
|
||||
// Get real size before compression
|
||||
[$unc_len, $fileModTime] =
|
||||
$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);
|
||||
|
||||
// Decide if we will compress
|
||||
$compressionMethod = $this->getCompressionMethod($unc_len, $memLimit, $isDir, $isSymlink);
|
||||
|
||||
if ($isVirtual)
|
||||
{
|
||||
Factory::getLog()->debug(' Virtual add:' . $targetName . ' (' . $unc_len . ') - ' . $compressionMethod);
|
||||
}
|
||||
|
||||
/* "Local file header" segment. */
|
||||
|
||||
$crc = $this->getCRCForEntity($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
|
||||
|
||||
$storedName = $targetName;
|
||||
|
||||
if (!$isSymlink && $isDir)
|
||||
{
|
||||
$storedName .= "/";
|
||||
$unc_len = 0;
|
||||
}
|
||||
|
||||
// Test for non-existing or unreadable files
|
||||
$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
|
||||
|
||||
// Default compressed (archived) length = uncompressed length – valid unless we can actually compress the data.
|
||||
$c_len = $unc_len;
|
||||
|
||||
// If we have to compress, read the data in memory and compress it
|
||||
if ($compressionMethod == 8)
|
||||
{
|
||||
$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
|
||||
|
||||
// The method modifies $compressionMethod to 0 (uncompressed) or 1 (Deflate) but the ZIP format needs it
|
||||
// to be 0 (uncompressed) or 8 (Deflate). So I just multiply by 8.
|
||||
$compressionMethod *= 8;
|
||||
}
|
||||
|
||||
// Get the hex time.
|
||||
$hexdtime = pack('V', $this->unix2DOSTime($fileModTime));
|
||||
|
||||
// If it's a split ZIP file, we've got to make sure that the header can fit in the part
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Get header size, taking into account any extra header necessary
|
||||
$header_size = 30 + akstrlen($storedName);
|
||||
|
||||
// Compare to free part space
|
||||
$free_space = $this->getPartFreeSize();
|
||||
|
||||
if ($free_space <= $header_size)
|
||||
{
|
||||
// Not enough space on current part, create new part
|
||||
$this->createAndOpenNewPart();
|
||||
}
|
||||
}
|
||||
|
||||
$old_offset = @ftell($this->fp);
|
||||
|
||||
if ($this->useSplitArchive && ($old_offset == 0))
|
||||
{
|
||||
// Because in split ZIPs we have the split ZIP marker in the first four bytes.
|
||||
@fseek($this->fp, 4);
|
||||
$old_offset = @ftell($this->fp);
|
||||
}
|
||||
|
||||
// Get the file name length in bytes
|
||||
$fn_length = akstrlen($storedName);
|
||||
|
||||
$this->fwrite($this->fp, $this->fileHeaderSignature); /* Begin creating the ZIP data. */
|
||||
|
||||
/* Version needed to extract. */
|
||||
if (!$isSymlink)
|
||||
{
|
||||
$this->fwrite($this->fp, "\x14\x00");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->fwrite($this->fp, "\x0a\x03");
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, pack('v', 2048)); /* General purpose bit flag. Bit 11 set = use UTF-8 encoding for filenames & comments */
|
||||
$this->fwrite($this->fp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
|
||||
$this->fwrite($this->fp, $hexdtime); /* Last modification time/date. */
|
||||
$this->fwrite($this->fp, $crc); /* CRC 32 information. */
|
||||
$this->fwrite($this->fp, pack('V', $c_len)); /* Compressed filesize. */
|
||||
$this->fwrite($this->fp, pack('V', $unc_len)); /* Uncompressed filesize. */
|
||||
$this->fwrite($this->fp, pack('v', $fn_length)); /* Length of filename. */
|
||||
$this->fwrite($this->fp, pack('v', 0)); /* Extra field length. */
|
||||
$this->fwrite($this->fp, $storedName); /* File name. */
|
||||
|
||||
// Cache useful information about the file
|
||||
if (!$isDir && !$isSymlink && !$isVirtual)
|
||||
{
|
||||
$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
|
||||
$configuration->set('volatile.engine.archiver.hexdtime', $hexdtime);
|
||||
$configuration->set('volatile.engine.archiver.crc', $crc);
|
||||
$configuration->set('volatile.engine.archiver.c_len', $c_len);
|
||||
$configuration->set('volatile.engine.archiver.fn_length', $fn_length);
|
||||
$configuration->set('volatile.engine.archiver.old_offset', $old_offset);
|
||||
$configuration->set('volatile.engine.archiver.storedName', $storedName);
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred compression method for a file
|
||||
*
|
||||
* @param int $fileSize File size in bytes
|
||||
* @param int $memLimit Memory limit in bytes
|
||||
* @param bool $isDir Is it a directory?
|
||||
* @param bool $isSymlink Is it a symlink?
|
||||
*
|
||||
* @return int Compression method to use
|
||||
*/
|
||||
protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
|
||||
{
|
||||
// ZIP uses 0 for uncompressed and 8 for GZip Deflate whereas the parent method returns 0 and 1 respectively
|
||||
return 8 * parent::getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the CRC-32 checksum
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
*
|
||||
* @return int The CRC-32
|
||||
*/
|
||||
protected function getCRCForEntity(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
|
||||
{
|
||||
// No hash? No CRC32!
|
||||
if (!function_exists('hash'))
|
||||
{
|
||||
return pack('V', 0);
|
||||
}
|
||||
|
||||
// Do I need to reverse the endianness of CRC32b data?
|
||||
static $reverseEndianness = null;
|
||||
|
||||
if ($reverseEndianness === null)
|
||||
{
|
||||
$reverseEndianness = hash('crc32b', 'The quick brown fox jumped over the lazy dog.', true) === pack('N', 2191738434);
|
||||
}
|
||||
|
||||
$entityType = 'file';
|
||||
$loggedName = null;
|
||||
|
||||
// Directories: dummy CRC-32
|
||||
if (!$isSymlink && $isDir)
|
||||
{
|
||||
$entityType = 'folder';
|
||||
$crc = pack('V', 0);
|
||||
}
|
||||
// Symlinks: CRC32 of the link source
|
||||
elseif ($isSymlink)
|
||||
{
|
||||
$entityType = 'symlink';
|
||||
$crc = hash('crc32b', @readlink($sourceNameOrData) ?: '', true);
|
||||
}
|
||||
// Virtual files: CRC32 of the contents
|
||||
elseif ($isVirtual)
|
||||
{
|
||||
$entityType = 'virtual file';
|
||||
$loggedName = sprintf('of size %u', strlen($sourceNameOrData));
|
||||
$crc = hash('crc32b', $sourceNameOrData ?: '', true);
|
||||
}
|
||||
// Files: CRC32 of the file contents
|
||||
else
|
||||
{
|
||||
// Get the CRC32 for the file
|
||||
$crc = function_exists("hash_file") ? @hash_file('crc32b', $sourceNameOrData, true) : null;
|
||||
|
||||
// If the file was unreadable skip it
|
||||
if ($crc === false)
|
||||
{
|
||||
throw new WarningException('Could not calculate CRC32 for ' . $sourceNameOrData . '. Looks like it is an unreadable file.');
|
||||
}
|
||||
|
||||
// If hash_file is not available use a fake CRC32
|
||||
$crc = $crc ?: pack('V', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* If CRC32 returns Big Endian data I'll have to convert it to Little Endian, as required by ZIP.
|
||||
*
|
||||
* I cannot unpack as Big Endian and repack as Little Endian. The intermediate conversion to integer might
|
||||
* overflow 32-bit versions of PHP as its internal integer type is platform-dependent.
|
||||
*
|
||||
* I cannot reverse the string using string functions because the internal character encoding may be something
|
||||
* other than ASCII / 8-bit and mb_string might not be available.
|
||||
*
|
||||
* Instead, I am unpacking the binary string as an array of unsigned bytes and then repacking the bytes, in
|
||||
* reverse order, into a binary string. pack() and unpack() are not affected by the character encoding. Using
|
||||
* unsigned bytes guarantees they will fit into internal integer variables regardless of the platform used.
|
||||
*/
|
||||
if ($crc !== "\000\000\000\000" && $reverseEndianness)
|
||||
{
|
||||
$temp = array_values(unpack('C4', $crc));
|
||||
$crc = pack('C*', $temp[3], $temp[2], $temp[1], $temp[0]);
|
||||
}
|
||||
|
||||
// Log the calculated CRC32 if the site is in debug mode
|
||||
if (defined('AKEEBADEBUG'))
|
||||
{
|
||||
$asHexChars = array_map(
|
||||
function ($c) {
|
||||
return dechex($c);
|
||||
}, unpack('C*', $crc)
|
||||
);
|
||||
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'%s %s - CRC32 = %s',
|
||||
$entityType,
|
||||
$loggedName ?? $sourceNameOrData,
|
||||
implode('', $reverseEndianness ? array_reverse($asHexChars) : $asHexChars)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UNIX timestamp to a 4-byte DOS date and time format
|
||||
* (date in high 2-bytes, time in low 2-bytes allowing magnitude
|
||||
* comparison).
|
||||
*
|
||||
* @param integer $unixtime The current UNIX timestamp.
|
||||
*
|
||||
* @return integer The current date in a 4-byte DOS format.
|
||||
*/
|
||||
protected function unix2DOSTime($unixtime = null)
|
||||
{
|
||||
$timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);
|
||||
|
||||
if ($timearray['year'] < 1980)
|
||||
{
|
||||
$timearray['year'] = 1980;
|
||||
$timearray['mon'] = 1;
|
||||
$timearray['mday'] = 1;
|
||||
$timearray['hours'] = 0;
|
||||
$timearray['minutes'] = 0;
|
||||
$timearray['seconds'] = 0;
|
||||
}
|
||||
|
||||
return (($timearray['year'] - 1980) << 25) |
|
||||
($timearray['mon'] << 21) |
|
||||
($timearray['mday'] << 16) |
|
||||
($timearray['hours'] << 11) |
|
||||
($timearray['minutes'] << 5) |
|
||||
($timearray['seconds'] >> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new part for the spanned archive
|
||||
*
|
||||
* @param bool $finalPart Is this the final archive part?
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
protected function createNewPartFile($finalPart = false)
|
||||
{
|
||||
// Close any open file pointers
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (is_resource($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
// Remove the just finished part from the list of resumable offsets
|
||||
$this->removeFromOffsetsList($this->_dataFileName);
|
||||
|
||||
// Set the file pointers to null
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
|
||||
// Push the previous part if we have to post-process it immediately
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0))
|
||||
{
|
||||
$this->finishedPart[] = $this->_dataFileName;
|
||||
}
|
||||
|
||||
// Add the part's size to our rolling sum
|
||||
clearstatcache();
|
||||
$this->totalCompressedSize += filesize($this->_dataFileName);
|
||||
$this->totalParts++;
|
||||
$this->currentPartNumber = $this->totalParts;
|
||||
|
||||
if ($finalPart)
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.zip';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.z' . sprintf('%02d', $this->currentPartNumber);
|
||||
}
|
||||
|
||||
Factory::getLog()->info('Creating new ZIP part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
|
||||
|
||||
// Inform the backup engine that we have changed the multipart number
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->updateMultipart($this->totalParts);
|
||||
|
||||
// Try to remove any existing file
|
||||
@unlink($this->_dataFileName);
|
||||
|
||||
// Touch the new file
|
||||
$result = @touch($this->_dataFileName);
|
||||
|
||||
@chmod($this->_dataFileName, $this->getPermissions());
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the optimal chunk size for CRC32 calculations and file processing
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function findOptimalChunkSize()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// The user has entered their own preference
|
||||
if ($configuration->get('engine.archiver.common.chunk_size', 0) > 0)
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = AKEEBA_CHUNK;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the PHP memory limit
|
||||
$memLimit = $this->getMemoryLimit();
|
||||
|
||||
// Can't get a PHP memory limit? Use 2Mb chunks (fairly large, right?)
|
||||
if (is_null($memLimit))
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!function_exists("memory_get_usage"))
|
||||
{
|
||||
// PHP can't report memory usage, use a conservative 512Kb
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = 524288;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// PHP *can* report memory usage, see if there's enough available memory
|
||||
$availableRAM = $memLimit - memory_get_usage();
|
||||
|
||||
if ($availableRAM > 0)
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = $availableRAM * 0.5;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// NEGATIVE AVAILABLE MEMORY?!! Some borked PHP implementations also return the size of the httpd footprint.
|
||||
if (($memLimit - 6291456) > 0)
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = $memLimit - 6291456;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If all else fails, use 2Mb and cross your fingers
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Central Directory temporary file
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
private function createCentralDirectoryTempFile()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$this->centralDirectoryFilename = tempnam($configuration->get('akeeba.basic.output_directory'), 'akzcd');
|
||||
$this->centralDirectoryFilename = basename($this->centralDirectoryFilename);
|
||||
$pos = strrpos($this->centralDirectoryFilename, '/');
|
||||
|
||||
if ($pos !== false)
|
||||
{
|
||||
$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
|
||||
}
|
||||
|
||||
$pos = strrpos($this->centralDirectoryFilename, '\\');
|
||||
|
||||
if ($pos !== false)
|
||||
{
|
||||
$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
|
||||
}
|
||||
|
||||
$this->centralDirectoryFilename = Factory::getTempFiles()->registerTempFile($this->centralDirectoryFilename);
|
||||
|
||||
Factory::getLog()->debug(__CLASS__ . " :: CntDir Tempfile = " . $this->centralDirectoryFilename);
|
||||
|
||||
// Create temporary file
|
||||
if (!@touch($this->centralDirectoryFilename))
|
||||
{
|
||||
throw new ErrorException("Could not open temporary file for ZIP archiver. Please check your temporary directory's permissions!");
|
||||
}
|
||||
|
||||
@chmod($this->centralDirectoryFilename, $this->getPermissions());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"_information": {
|
||||
"title": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.dereference_symlinks": {
|
||||
"default": "0",
|
||||
"type": "bool",
|
||||
"title": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.part_size": {
|
||||
"default": "0",
|
||||
"type": "integer",
|
||||
"min": "0",
|
||||
"max": "2147483648",
|
||||
"shortcuts": "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_PARTSIZE_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.permissions": {
|
||||
"default": "0666",
|
||||
"type": "enum",
|
||||
"enumkeys": "COM_AKEEBA_CONFIG_PERMISSIONS_0600|COM_AKEEBA_CONFIG_PERMISSIONS_0644|COM_AKEEBA_CONFIG_PERMISSIONS_0666",
|
||||
"enumvalues": "0600|0644|0666",
|
||||
"title": "COM_AKEEBA_CONFIG_PERMISSIONS_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_PERMISSIONS_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.chunk_size": {
|
||||
"default": "1048576",
|
||||
"type": "integer",
|
||||
"min": "65536",
|
||||
"max": "10485760",
|
||||
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.big_file_threshold": {
|
||||
"default": "1048576",
|
||||
"type": "integer",
|
||||
"min": "65536",
|
||||
"max": "10485760",
|
||||
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"_information": {
|
||||
"title": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.dereference_symlinks": {
|
||||
"default": "0",
|
||||
"type": "bool",
|
||||
"title": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.part_size": {
|
||||
"default": "0",
|
||||
"type": "integer",
|
||||
"min": "0",
|
||||
"max": "2147483648",
|
||||
"shortcuts": "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_PARTSIZE_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.chunk_size": {
|
||||
"default": "1048576",
|
||||
"type": "integer",
|
||||
"min": "65536",
|
||||
"max": "10485760",
|
||||
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.permissions": {
|
||||
"default": "0666",
|
||||
"type": "enum",
|
||||
"enumkeys": "COM_AKEEBA_CONFIG_PERMISSIONS_0600|COM_AKEEBA_CONFIG_PERMISSIONS_0644|COM_AKEEBA_CONFIG_PERMISSIONS_0666",
|
||||
"enumvalues": "0600|0644|0666",
|
||||
"title": "COM_AKEEBA_CONFIG_PERMISSIONS_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_PERMISSIONS_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.common.big_file_threshold": {
|
||||
"default": "1048576",
|
||||
"type": "integer",
|
||||
"min": "65536",
|
||||
"max": "10485760",
|
||||
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION"
|
||||
},
|
||||
"engine.archiver.zip.cd_glue_chunk_size": {
|
||||
"default": "1048576",
|
||||
"type": "integer",
|
||||
"min": "65536",
|
||||
"max": "10485760",
|
||||
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
|
||||
"scale": "1048576",
|
||||
"uom": "MB",
|
||||
"title": "COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_TITLE",
|
||||
"description": "COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_DESCRIPTION"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user