浏览代码

N.612 Backup files could not exceed 4Gb (technology limitation). The fix consists in archiving the backup as a tar.gz instead of a zip. As a consequence, installing iTop now requires TWO additional PHP modules: phar/zlib. The zip module remains mandatory because it is used in other places. The restore utility accepts both legacy zip files and brand new tar.gz files. DBBackup::CreateZip is deprecated in favor of DBBackup::CreateCompressedBackup. DBRestore::RestoreFromZip is deprecated in favor of DBRestore::RestoreFromCompressedFile (which autodetects the format for backward compatibility).

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4803 a333f486-631f-4898-b8df-5754b55c2be0
romainq 8 年之前
父节点
当前提交
5528688133

+ 3 - 3
datamodels/2.x/itop-backup/ajax.backup.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2013-2016 Combodo SARL
+// Copyright (C) 2013-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Backup from an interactive session
  *
- * @copyright   Copyright (C) 2013-2016 Combodo SARL
+ * @copyright   Copyright (C) 2013-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -147,7 +147,7 @@ EOF
 	
 				$sBackupDir = APPROOT.'data/backups/';
 				$sBackupFile = $sBackupDir.$sFile;
-				$sRes = $oDBRS->RestoreFromZip($sBackupFile, $sEnvironment);
+				$sRes = $oDBRS->RestoreFromCompressedBackup($sBackupFile, $sEnvironment);
 	
 				IssueLog::Info('Backup Restore - Done, releasing the LOCK');
 				$oRestoreMutex->Unlock();

+ 4 - 6
datamodels/2.x/itop-backup/backup.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2014 Combodo SARL
+// Copyright (C) 2014-2017 Combodo SARL
 //
 //   This program is free software; you can redistribute it and/or modify
 //   it under the terms of the GNU General Public License as published by
@@ -176,13 +176,12 @@ $sBackupFile =  utils::ReadParam('backup_file', $sDefaultBackupFileName, true, '
 $oBackup = new MyDBBackup($oP);
 $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
 $sBackupFile = $oBackup->MakeName($sBackupFile);
-$sZipArchiveFile = $sBackupFile.'.zip';
 
 $bSimulate = utils::ReadParam('simulate', false, true);
 $res = false;
 if ($bSimulate)
 {
-	$oP->p("Simulate: would create file '$sZipArchiveFile'");
+	$oP->p("Simulate: would create file '$sBackupFile'");
 }
 elseif (MetaModel::GetConfig()->Get('demo_mode'))
 {
@@ -190,11 +189,10 @@ elseif (MetaModel::GetConfig()->Get('demo_mode'))
 }
 else
 {
-	$oBackup->CreateZip($sZipArchiveFile);
+	$oBackup->CreateCompressedBackup($sBackupFile);
 }
 if ($res && $bDownloadBackup)
 {
-	$oBackup->DownloadBackup($sZipArchiveFile);
+	$oBackup->DownloadBackup($sBackupFile);
 }
 $oP->output();
-?>

+ 2 - 2
datamodels/2.x/itop-backup/check-backup.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2014 Combodo SARL
+// Copyright (C) 2014-2017 Combodo SARL
 //
 //   This program is free software; you can redistribute it and/or modify
 //   it under the terms of the GNU General Public License as published by
@@ -100,7 +100,7 @@ function MakeArchiveFileName($iRefTime = null)
 		$sBackupFile = strftime($sBackupFile, $iRefTime);
 	}
 
-	return $sBackupFile.'.zip';
+	return $sBackupFile;
 }
 
 

+ 38 - 11
datamodels/2.x/itop-backup/dbrestore.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2014 Combodo SARL
+// Copyright (C) 2014-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -88,19 +88,47 @@ class DBRestore extends DBBackup
 		}
 	}
 
+	/**
+	 * @deprecated Use RestoreFromCompressedBackup instead
+	 * @param $sZipFile
+	 * @param string $sEnvironment
+	 */
 	public function RestoreFromZip($sZipFile, $sEnvironment = 'production')
 	{
-		$this->LogInfo("Starting restore of ".basename($sZipFile));
+		$this->RestoreFromCompressedBackup($sZipFile, $sEnvironment);
+	}
 
-		$oZip = new ZipArchiveEx();
-		$res = $oZip->open($sZipFile);
+	/**
+	 * @param $sFile A file with the extension .zip or .tar.gz
+	 * @param string $sEnvironment Target environment
+	 */
+	public function RestoreFromCompressedBackup($sFile, $sEnvironment = 'production')
+	{
+		$this->LogInfo("Starting restore of ".basename($sFile));
+
+		$sNormalizedFile = strtolower(basename($sFile));
+		if (substr($sNormalizedFile, -4) == '.zip')
+		{
+			$this->LogInfo('zip file detected');
+			$oArchive = new ZipArchiveEx();
+			$res = $oArchive->open($sFile);
+		}
+		elseif (substr($sNormalizedFile, -7) == '.tar.gz')
+		{
+			$this->LogInfo('tar.gz file detected');
+			$oArchive = new TarGzArchive($sFile);
+		}
+		else
+		{
+			throw new Exception('Unsupported format for a backup file: '.$sFile);
+		}
 
 		// Load the database
 		//
 		$sDataDir = tempnam(SetupUtils::GetTmpDir(), 'itop-');
 		unlink($sDataDir); // I need a directory, not a file...
 		SetupUtils::builddir($sDataDir); // Here is the directory
-		$oZip->extractTo($sDataDir, 'itop-dump.sql');
+		$oArchive->extractFileTo($sDataDir, 'itop-dump.sql');
 		$sDataFile = $sDataDir.'/itop-dump.sql';
 		$this->LoadDatabase($sDataFile);
 		unlink($sDataFile);
@@ -108,10 +136,10 @@ class DBRestore extends DBBackup
 		// Update the code
 		//
 		$sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml';
-		if ($oZip->locateName('delta.xml') !== false)
+		if ($oArchive->hasFile('delta.xml') !== false)
 		{
 			// Extract and rename delta.xml => <env>.delta.xml;
-			file_put_contents($sDeltaFile, $oZip->getFromName('delta.xml'));
+			file_put_contents($sDeltaFile, $oArchive->getFromName('delta.xml'));
 		}
 		else
 		{
@@ -121,18 +149,17 @@ class DBRestore extends DBBackup
 		{
 			SetupUtils::rrmdir(APPROOT.'data/production-modules/');
 		}
-		if ($oZip->locateName('production-modules/') !== false)
+		if ($oArchive->hasDir('production-modules/') !== false)
 		{
-			$oZip->extractDirTo(APPROOT.'data/', 'production-modules/');
+			$oArchive->extractDirTo(APPROOT.'data/', 'production-modules/');
 		}
 
 		$sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php';
 		@chmod($sConfigFile, 0770); // Allow overwriting the file
-		$oZip->extractTo(APPROOT.'conf/'.$sEnvironment, 'config-itop.php');
+		$oArchive->extractFileTo(APPROOT.'conf/'.$sEnvironment, 'config-itop.php');
 		@chmod($sConfigFile, 0444); // Read-only
 
 		$oEnvironment = new RunTimeEnvironment($sEnvironment);
 		$oEnvironment->CompileFrom($sEnvironment);
 	}
 }
-

+ 13 - 6
datamodels/2.x/itop-backup/main.itop-backup.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2014-2016 Combodo SARL
+// Copyright (C) 2014-2017 Combodo SARL
 //
 //   This program is free software; you can redistribute it and/or modify
 //   it under the terms of the GNU General Public License as published by
@@ -82,11 +82,18 @@ class DBBackupScheduled extends DBBackup
 	{
 		$aFiles = array();
 		$aTimes = array();
+		// Legacy format -limited to 4 Gb
 		foreach(glob($sBackupDir.'*.zip') as $sFilePath)
 		{
 			$aFiles[] = $sFilePath;
 			$aTimes[] = filemtime($sFilePath); // unix time
 		}
+		// Modern format
+		foreach(glob($sBackupDir.'*.tar.gz') as $sFilePath)
+		{
+			$aFiles[] = $sFilePath;
+			$aTimes[] = filemtime($sFilePath); // unix time
+		}
 		array_multisort($aTimes, $aFiles);
 	
 		return $aFiles;
@@ -156,15 +163,15 @@ class BackupExec implements iScheduledProcess
 			//
 			$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
 	
-			$sBackupFile = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'file_name_format', '__DB__-%Y-%m-%d_%H_%M');
-			$sName = $oBackup->MakeName($sBackupFile);
+			$sBackupFileFormat = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'file_name_format', '__DB__-%Y-%m-%d_%H_%M');
+			$sName = $oBackup->MakeName($sBackupFileFormat);
 			if ($sName == '')
 			{
 				$sName = $oBackup->MakeName(BACKUP_DEFAULT_FORMAT);
 			}
-			$sZipFile = $this->sBackupDir.$sName.'.zip';
+			$sBackupFile = $this->sBackupDir.$sName;
 			$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
-			$oBackup->CreateZip($sZipFile, $sSourceConfigFile);
+			$oBackup->CreateCompressedBackup($sBackupFile, $sSourceConfigFile);
 		}
 		catch (Exception $e)
 		{
@@ -172,7 +179,7 @@ class BackupExec implements iScheduledProcess
 			throw $e;
 		}
 		$oMutex->Unlock();
-		return "Created the backup: $sZipFile";
+		return "Created the backup: $sBackupFile";
 	}
 
 	/*

+ 6 - 6
setup/applicationinstaller.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -30,7 +30,7 @@ require_once(APPROOT.'setup/backup.class.inc.php');
  * while displaying a progress bar, or in an unattended manner
  * (for example from the command line), to run all the steps
  * in one go.
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -172,7 +172,7 @@ class ApplicationInstaller
 				
 				case 'backup':
 				$aPreinstall = $this->oParams->Get('preinstall');
-				// __DB__-%Y-%m-%d.zip
+				// __DB__-%Y-%m-%d
 				$sDestination = $aPreinstall['backup']['destination'];
 				$sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
 				$aDBParams = $this->oParams->Get('database');
@@ -412,11 +412,11 @@ class ApplicationInstaller
 		return $sReport;
 	}
 
-	protected static function DoBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sBackupFile, $sSourceConfigFile)
+	protected static function DoBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sBackupFileFormat, $sSourceConfigFile)
 	{
 		$oBackup = new SetupDBBackup($sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix);
-		$sZipFile = $oBackup->MakeName($sBackupFile);
-		$oBackup->CreateZip($sZipFile, $sSourceConfigFile);
+		$sTargetFile = $oBackup->MakeName($sBackupFileFormat);
+		$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
 	}
 
 	

+ 380 - 32
setup/backup.class.inc.php

@@ -15,12 +15,54 @@
 //
 //   You should have received a copy of the GNU Affero General Public License
 //   along with iTop. If not, see <http://www.gnu.org/licenses/>
-/**
- * Handles adding directories into a Zip archive
- */
+
+interface BackupArchive
+{
+	/**
+	 * @param string $sFile
+	 * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
+	 */
+	public function hasFile($sFile);
+
+	/**
+	 * @param string $sDirectory
+	 * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
+	 */
+	public function hasDir($sDirectory);
+
+	/**
+	 * @param string $sDestinationDir
+	 * @param string $sArchiveFile
+	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
+	 */
+	public function extractFileTo($sDestinationDir, $sArchiveFile);
+
+	/**
+	 * Extract a whole directory from the archive.
+	 * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/')
+	 * @param string $sDestinationDir
+	 * @param string $sArchiveDir Note: must start and end with a slash !!
+	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
+	 */
+	public function extractDirTo($sDestinationDir, $sArchiveDir);
+
+	/**
+	 * Returns the entry contents using its name
+	 * @param string $name Name of the entry
+	 * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read.
+	 * @param int $flags [optional] The flags to use to open the archive. the following values may be ORed to it. <b>ZipArchive::FL_UNCHANGED</b>
+	 * @return string the contents of the entry on success or <b>FALSE</b> on failure.
+	 */
+	public function getFromName($name, $length = 0, $flags = null);
+}
+
 if (class_exists('ZipArchive')) // The setup must be able to start even if the "zip" extension is not loaded
 {
-	class ZipArchiveEx extends ZipArchive
+	/**
+	 * Handles adding directories into a Zip archive, and a unified API for archive read
+	 * suggested enhancement: refactor the API for writing as well
+	 */
+	class ZipArchiveEx extends ZipArchive implements BackupArchive
 	{
 		public function addDir($sDir, $sZipDir = '')
 		{
@@ -53,6 +95,34 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the "
 			}
 		}
 		/**
+		 * @param string $sFile
+		 * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
+		 */
+		public function hasFile($sFile)
+		{
+			return ($this->locateName($sFile) !== false);
+		}
+
+		/**
+		 * @param string $sDirectory
+		 * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
+		 */
+		public function hasDir($sDirectory)
+		{
+			return ($this->locateName($sDirectory) !== false);
+		}
+
+		/**
+		 * @param string $sDestinationDir
+		 * @param string $sArchiveFile
+		 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
+		 */
+		public function extractFileTo($sDestinationDir, $sArchiveFile)
+		{
+			return $this->extractTo($sDestinationDir, $sArchiveFile);
+		}
+
+		/**
 		 * Extract a whole directory from the archive.
 		 * Usage: $oZip->extractDirTo('/var/www/html/itop/data', '/production-modules/')
 		 * @param string $sDestinationDir
@@ -72,7 +142,7 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the "
 					$aFiles[] = $sEntry;
 				}
 			}
-			// Extract only the selcted files
+			// Extract only the selected files
 			if ((count($aFiles)  > 0) && ($this->extractTo($sDestinationDir, $aFiles) === true)) 
 			{
 				return true;
@@ -169,58 +239,170 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the "
 			$sFileName = strftime($sFileName);
 			return $sFileName;
 		}
-	
+
+		/**
+		 * @deprecated 2.4.0 Zip files are limited to 4 Gb, use CreateCompressedBackup to create tar.gz files
+		 * @param $sZipFile
+		 * @param null $sSourceConfigFile
+		 */
 		public function CreateZip($sZipFile, $sSourceConfigFile = null)
 		{
+			$aContents = array();
+
 			// Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
 			// (delete it before spawning a process)
 			$sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
 			$this->LogInfo("Data file: '$sDataFile'");
-	
-			$aContents = array();
+			$this->DoBackup($sDataFile);
 			$aContents[] = array(
 				'source' => $sDataFile,
 				'dest' => 'itop-dump.sql',
 			);
-	
+
+			foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile)
+			{
+				$aContents[] = array(
+					'source' => $sSourceFile,
+					'dest' => $sArchiveFile,
+				);
+			}
+
+			$this->DoZip($aContents, $sZipFile);
+
+			// Windows/IIS: the data file has been created by the spawned process...
+			//   trying to delete it will issue a warning, itself stopping the setup abruptely
+			@unlink($sDataFile);
+		}
+
+		/**
+		 * @param string $sTargetFile Path and name, without the extension
+		 * @param string|null $sSourceConfigFile Configuration file to embed into the backup, if not the current one
+		 */
+		public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null)
+		{
+			$this->LogInfo("Creating backup: '$sTargetFile.tar.gz'");
+
+			// Note: PharData::compress strips averything after the first dot found in the name of the tar, then it adds .tar.gz
+			//       Hence, we have to create our own file in the target directory, and rename it when the process is complete
+			$sTarFile = str_replace('.', '_', $sTargetFile).'.tar';
+			$this->LogInfo("Tar file: '$sTarFile'");
+			$oArchive = new PharData($sTarFile);
+
+			// Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
+			// (delete it before spawning a process)
+			// Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
+			// (delete it before spawning a process)
+			$sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
+			$this->LogInfo("Data file: '$sDataFile'");
+			$this->DoBackup($sDataFile);
+
+			$oArchive->addFile($sDataFile, 'itop-dump.sql');
+			// todo: reduce disk space needed by the operation by piping the output of mysqldump directly into the tar
+			// tip1 : this syntax works fine (did not work with addFile)
+			//$oArchive->buildFromIterator(
+			//	new ArrayIterator(
+			//		array('production.delta.xml' => fopen(ROOTDIR.'production.delta.xml', 'rb'))
+			//	)
+			//);
+			// tip2 : use the phar stream by redirecting the output of mysqldump into
+			//        phar://var/www/itop/data/backups/manual/trunk_pro-2017-07-05_15_10.tar.gz/itop-dump.sql
+			//
+			//	new ArrayIterator(
+			//		array('production.delta.xml' => fopen(ROOTDIR.'production.delta.xml', 'rb'))
+			//	)
+			//);
+
+			// Windows/IIS: the data file has been created by the spawned process...
+			//   trying to delete it will issue a warning, itself stopping the setup abruptely
+			@unlink($sDataFile);
+
+			foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile)
+			{
+				if (is_dir($sSourceFile))
+				{
+					$this->LogInfo("Adding directory into tar file: '$sSourceFile', recorded as '$sArchiveFile'");
+					// Note: Phar::buildFromDirectory does not allow to specify a destination subdirectory
+					//       Hence we have to add all files one by one
+					$sSourceDir = realpath($sSourceFile);
+					$sArchiveDir = trim($sArchiveFile, '/');
+
+					$oDirectoryIterator = new RecursiveDirectoryIterator($sSourceDir, RecursiveDirectoryIterator::SKIP_DOTS);
+					$oAllFiles = new RecursiveIteratorIterator($oDirectoryIterator);
+					foreach ($oAllFiles as $oSomeFile)
+					{
+						if ($oSomeFile->isDir()) continue;
+
+						// Replace the local path by the archive path - the resulting string starts with a '/'
+						$sRelativePathName = substr($oSomeFile->getRealPath(), strlen($sSourceDir));
+						// Under Windows realpath gives a mix of backslashes and slashes
+						$sRelativePathName = str_replace('\\', '/', $sRelativePathName);
+						$sArchiveFile = $sArchiveDir.$sRelativePathName;
+						$oArchive->addFile($oSomeFile->getPathName(), $sArchiveFile);
+					}
+				}
+				else
+				{
+					$this->LogInfo("Adding file into tar file: '$sSourceFile', recorded as '$sArchiveFile'");
+					$oArchive->addFile($sSourceFile, $sArchiveFile);
+				};
+			}
+
+			if (file_exists($sTarFile.'.gz'))
+			{
+				// Prevent the gzip compression from failing -> the whole operation is an overwrite
+				$this->LogInfo("Overwriting tar.gz: '$sTarFile'");
+				unlink($sTarFile.'.gz');
+			}
+			// zlib is a must!
+			$oArchive->compress(Phar::GZ);
+
+			// Cleanup
+			unset($oArchive);
+			unlink($sTarFile);
+
+			if ($sTargetFile != $sTarFile)
+			{
+				// Give the file the expected name
+				if (file_exists($sTargetFile.'.gz'))
+				{
+					// Remove it -> the whole operation is an overwrite
+					$this->LogInfo("Overwriting tar.gz: '$sTargetFile'");
+					unlink($sTargetFile.'.gz');
+				}
+				rename($sTarFile.'.gz', $sTargetFile.'.tar.gz');
+			}
+		}
+
+		/**
+		 * List files to store into the archive, in addition to the SQL dump
+		 * @return array of sArchiveName => sFilePath
+		 */
+		protected function GetAdditionalFiles($sSourceConfigFile)
+		{
+			$aRet = array();
 			if (is_null($sSourceConfigFile))
 			{
 				$sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile();
 			}
 			if (!empty($sSourceConfigFile))
 			{
-				$aContents[] = array(
-					'source' => $sSourceConfigFile,
-					'dest' => 'config-itop.php',
-				);
+				$aRet['config-itop.php'] = $sSourceConfigFile;
 			}
-	
-			$this->DoBackup($sDataFile);
-	
+
 			$sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml';
 			if (file_exists($sDeltaFile))
 			{
-				$aContents[] = array(
-					'source' => $sDeltaFile,
-					'dest' => 'delta.xml',
-				);
+				$aRet['delta.xml'] = $sDeltaFile;
 			}
 			$sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/';
 			if (is_dir($sExtraDir))
 			{
-				$aContents[] = array(
-					'source' => $sExtraDir,
-					'dest' => utils::GetCurrentEnvironment().'-modules/',
-				);
+				$sModules = utils::GetCurrentEnvironment().'-modules/';
+				$aRet[$sModules] = $sExtraDir;
 			}
-			
-			$this->DoZip($aContents, $sZipFile);
-			// Windows/IIS: the data file has been created by the spawned process...
-			//   trying to delete it will issue a warning, itself stopping the setup abruptely
-			@unlink($sDataFile);
+			return $aRet;
 		}
-	
-	
+
 		protected static function EscapeShellArg($sValue)
 		{
 			// Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation
@@ -324,7 +506,7 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the "
 		}
 	
 		/**
-		 * Helper to create a ZIP out of a data file and the configuration file
+		 * Helper to create a ZIP out of several files
 		 */	 	
 		protected function DoZip($aFiles, $sZipArchiveFile)
 		{
@@ -439,3 +621,169 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the "
 		}
 	}
 }
+
+class TarGzArchive implements BackupArchive
+{
+	/*
+	 * @var PharData
+	 */
+	protected $oPharArchive;
+	/*
+	 * string[]
+	 */
+	protected $aFiles = null;
+
+	public function __construct($sFile)
+	{
+		$this->oPharArchive = new PharData($sFile);
+	}
+
+	/**
+	 * @param string $sFile
+	 * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
+	 */
+	public function hasFile($sFile)
+	{
+		return $this->oPharArchive->offsetExists($sFile);
+	}
+
+	/**
+	 * @param string $sDirectory
+	 * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
+	 */
+	public function hasDir($sDirectory)
+	{
+		return $this->oPharArchive->offsetExists($sDirectory);
+	}
+
+	/**
+	 * @param string $sDestinationDir
+	 * @param string $sArchiveFile
+	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
+	 */
+	public function extractFileTo($sDestinationDir, $sArchiveFile)
+	{
+		return $this->oPharArchive->extractTo($sDestinationDir, $sArchiveFile, true);
+	}
+
+	/**
+	 * Extract a whole directory from the archive.
+	 * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/')
+	 * @param string $sDestinationDir
+	 * @param string $sArchiveDir Note: must start and end with a slash !!
+	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
+	 */
+	public function extractDirTo($sDestinationDir, $sArchiveDir)
+	{
+		$aFiles = array();
+		foreach ($this->getFiles($sArchiveDir) as $oFileInfo)
+		{
+			$aFiles[] = $oFileInfo->getRelativePath();
+		}
+		if ((count($aFiles) > 0) && ($this->oPharArchive->extractTo($sDestinationDir, $aFiles, true) === true))
+		{
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Returns the entry contents using its name
+	 * @param string $name Name of the entry
+	 * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read.
+	 * @param int $flags [optional] The flags to use to open the archive. the following values may be ORed to it. <b>ZipArchive::FL_UNCHANGED</b>
+	 * @return string the contents of the entry on success or <b>FALSE</b> on failure.
+	 */
+	public function getFromName($name, $length = 0, $flags = null)
+	{
+		$oFileInfo = $this->oPharArchive->offsetGet($name);
+		$sFile = $oFileInfo->getPathname();
+		$sRet = file_get_contents($sFile);
+		return $sRet;
+	}
+
+	/**
+	 * @param string|null $sArchivePath Path to search for
+	 * @return null
+	 */
+	public function getFiles($sArchivePath = null)
+	{
+		if ($this->aFiles === null)
+		{
+			// Initial load
+			$this->buildFileList();
+		}
+		if ($sArchivePath === null)
+		{
+			// Take them all
+			$aRet = $this->aFiles;
+		}
+		else
+		{
+			// Filter out files not in the given path
+			$aRet = array();
+			foreach ($this->aFiles as $oFileInfo)
+			{
+				if ($oFileInfo->isUnder($sArchivePath))
+				{
+					$aRet[] = $oFileInfo;
+				}
+			}
+		}
+		return $aRet;
+	}
+
+	/**
+	 * @param PharData|null $oPharData
+	 * @param string $sArchivePath Path relatively to the archive root
+	 */
+	protected function buildFileList($oPharData = null, $sArchivePath = '/')
+	{
+		if ($oPharData === null)
+		{
+			$oPharData = $this->oPharArchive;
+		}
+		foreach($oPharData as $oPharFileInfo)
+		{
+			if($oPharFileInfo->isDir())
+			{
+				$oSubDirectory = new PharData($oPharFileInfo->getPathname());
+				// Recurse
+				$this->buildFileList($oSubDirectory, $sArchivePath.'/'.$oPharFileInfo->getFileName());
+			}
+			else
+			{
+				$this->aFiles[] = new TarGzFileInfo($oPharFileInfo, $sArchivePath);
+			}
+		}
+	}
+}
+
+class TarGzFileInfo
+{
+	public function __construct(PharFileInfo $oFileInfo, $sArchivePath)
+	{
+		$this->oPharFileInfo = $oFileInfo;
+		$this->sArchivePath = trim($sArchivePath, '/');
+	}
+
+	protected $sArchivePath;
+	protected $oPharFileInfo;
+
+	public function getPathname()
+	{
+		return $this->oPharFileInfo->getPathname();
+	}
+
+	public function getRelativePath()
+	{
+		return $this->sArchivePath.'/'.$this->oPharFileInfo->getFilename();
+	}
+
+	public function isUnder($sArchivePath)
+	{
+		$sTestedPath = trim($sArchivePath, '/');
+		return (strpos($this->sArchivePath, $sTestedPath) === 0);
+	}
+}
+

+ 11 - 6
setup/setuputils.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -18,7 +18,7 @@
 
 /**
  * The standardized result of any pass/fail check performed by the setup
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -85,7 +85,7 @@ class SetupUtils
 		$aWritableDirsErrors = self::CheckWritableDirs(array('log', 'env-production', 'conf', 'data'));
 		$aResult = array_merge($aResult, $aWritableDirsErrors);
 		
-		$aMandatoryExtensions = array('mysqli', 'iconv', 'simplexml', 'soap', 'hash', 'json', 'session', 'pcre', 'dom', 'zip');
+		$aMandatoryExtensions = array('mysqli', 'iconv', 'simplexml', 'soap', 'hash', 'json', 'session', 'pcre', 'dom', 'phar', 'zlib', 'zip');
 		$aOptionalExtensions = array('mcrypt' => 'Strong encryption will not be used.',
 									 'ldap' => 'LDAP authentication will be disabled.',
 									 'gd' => 'PDF export will be disabled. Also, image resizing will be disabled on profile pictures (May increase database size).');
@@ -376,10 +376,15 @@ class SetupUtils
 
 		// zip extension
 		//
-		if (!extension_loaded('zip'))
+		if (!extension_loaded('phar'))
 		{
-			$sMissingExtensionLink = "<a href=\"http://www.php.net/manual/en/book.zip.php\" target=\"_blank\">zip</a>";
-			$aResult[] = new CheckResult(CheckResult::ERROR, "Missing PHP extension: zip", $sMissingExtensionLink);
+			$sMissingExtensionLink = "<a href=\"http://www.php.net/manual/en/book.phar.php\" target=\"_blank\">zip</a>";
+			$aResult[] = new CheckResult(CheckResult::ERROR, "Missing PHP extension: phar", $sMissingExtensionLink);
+		}
+		if (!extension_loaded('zlib'))
+		{
+			$sMissingExtensionLink = "<a href=\"http://www.php.net/manual/en/book.zlib.php\" target=\"_blank\">zip</a>";
+			$aResult[] = new CheckResult(CheckResult::ERROR, "Missing PHP extension: zlib", $sMissingExtensionLink);
 		}
 
 		// availability of exec()

+ 6 - 8
setup/wizardsteps.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2016 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -18,7 +18,7 @@
 
 /**
  * All the steps of the iTop installation wizard
- * @copyright   Copyright (C) 2010-2016 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -207,7 +207,7 @@ class WizStepInstallOrUpgrade extends WizardStep
 		$sPreviousVersionDir = '';
 		if ($sInstallMode == '')
 		{
-			$sDBBackupPath = APPROOT.'data/'.ITOP_APPLICATION.strftime('-backup-%Y-%m-%d.zip');
+			$sDBBackupPath = APPROOT.'data/'.ITOP_APPLICATION.strftime('-backup-%Y-%m-%d');
 			$bDBBackup = true;
 			$aPreviousInstance = SetupUtils::GetPreviousInstance(APPROOT);
 			if ($aPreviousInstance['found'])
@@ -2305,10 +2305,9 @@ class WizStepDone extends WizardStep
 		if (($this->oWizard->GetParameter('mode', '') == 'upgrade') && $this->oWizard->GetParameter('db_backup', false))
 		{
 			$sBackupDestination = $this->oWizard->GetParameter('db_backup_path', '');
-			if (file_exists($sBackupDestination))
+			if (file_exists($sBackupDestination.'.tar.gz'))
 			{
 				// To mitigate security risks: pass only the filename without the extension, the download will add the extension itself
-				$sTruncatedFilePath = preg_replace('/\.zip$/', '', $sBackupDestination);
 				$oPage->p('Your backup is ready');
 				$oPage->p('<a style="background:transparent;" href="'.utils::GetAbsoluteUrlAppRoot().'setup/ajax.dataloader.php?operation=async_action&step_class=WizStepDone&params[backup]='.urlencode($sTruncatedFilePath).'" target="_blank"><img src="../images/tar.png" style="border:0;vertical-align:middle;">&nbsp;Download '.basename($sBackupDestination).'</a>');
 			}
@@ -2418,14 +2417,13 @@ class WizStepDone extends WizardStep
 	
 	public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
 	{
-		$oParameters = new PHPParameters();
 		// For security reasons: add the extension now so that this action can be used to read *only* .zip files from the disk...
-		$sBackupFile = $aParameters['backup'].'.zip';
+		$sBackupFile = $aParameters['backup'].'.tar.gz';
 		if (file_exists($sBackupFile))
 		{
 			// Make sure there is NO output at all before our content, otherwise the document will be corrupted
 			$sPreviousContent = ob_get_clean();
-			$oPage->SetContentType('application/zip');
+			$oPage->SetContentType('application/gzip');
 			$oPage->SetContentDisposition('attachment', basename($sBackupFile));
 			$oPage->add(file_get_contents($sBackupFile));			
 		}