backup.class.inc.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. <?php
  2. // Copyright (C) 2010-2017 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. interface BackupArchive
  19. {
  20. /**
  21. * @param string $sFile
  22. * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
  23. */
  24. public function hasFile($sFile);
  25. /**
  26. * @param string $sDirectory
  27. * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
  28. */
  29. public function hasDir($sDirectory);
  30. /**
  31. * @param string $sDestinationDir
  32. * @param string $sArchiveFile
  33. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  34. */
  35. public function extractFileTo($sDestinationDir, $sArchiveFile);
  36. /**
  37. * Extract a whole directory from the archive.
  38. * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/')
  39. * @param string $sDestinationDir
  40. * @param string $sArchiveDir Note: must start and end with a slash !!
  41. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  42. */
  43. public function extractDirTo($sDestinationDir, $sArchiveDir);
  44. /**
  45. * Returns the entry contents using its name
  46. * @param string $name Name of the entry
  47. * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read.
  48. * @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>
  49. * @return string the contents of the entry on success or <b>FALSE</b> on failure.
  50. */
  51. public function getFromName($name, $length = 0, $flags = null);
  52. }
  53. if (class_exists('ZipArchive')) // The setup must be able to start even if the "zip" extension is not loaded
  54. {
  55. /**
  56. * Handles adding directories into a Zip archive, and a unified API for archive read
  57. * suggested enhancement: refactor the API for writing as well
  58. */
  59. class ZipArchiveEx extends ZipArchive implements BackupArchive
  60. {
  61. public function addDir($sDir, $sZipDir = '')
  62. {
  63. if (is_dir($sDir))
  64. {
  65. if ($dh = opendir($sDir))
  66. {
  67. // Add the directory
  68. if (!empty($sZipDir)) $this->addEmptyDir($sZipDir);
  69. // Loop through all the files
  70. while (($sFile = readdir($dh)) !== false)
  71. {
  72. // If it's a folder, run the function again!
  73. if (!is_file($sDir.$sFile))
  74. {
  75. // Skip parent and root directories
  76. if (($sFile !== ".") && ($sFile !== ".."))
  77. {
  78. $this->addDir($sDir.$sFile."/", $sZipDir.$sFile."/");
  79. }
  80. }
  81. else
  82. {
  83. // Add the files
  84. $this->addFile($sDir.$sFile, $sZipDir.$sFile);
  85. }
  86. }
  87. }
  88. }
  89. }
  90. /**
  91. * @param string $sFile
  92. * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
  93. */
  94. public function hasFile($sFile)
  95. {
  96. return ($this->locateName($sFile) !== false);
  97. }
  98. /**
  99. * @param string $sDirectory
  100. * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
  101. */
  102. public function hasDir($sDirectory)
  103. {
  104. return ($this->locateName($sDirectory) !== false);
  105. }
  106. /**
  107. * @param string $sDestinationDir
  108. * @param string $sArchiveFile
  109. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  110. */
  111. public function extractFileTo($sDestinationDir, $sArchiveFile)
  112. {
  113. return $this->extractTo($sDestinationDir, $sArchiveFile);
  114. }
  115. /**
  116. * Extract a whole directory from the archive.
  117. * Usage: $oZip->extractDirTo('/var/www/html/itop/data', '/production-modules/')
  118. * @param string $sDestinationDir
  119. * @param string $sZipDir Must start and end with a slash !!
  120. * @return boolean
  121. */
  122. public function extractDirTo($sDestinationDir, $sZipDir)
  123. {
  124. $aFiles = array();
  125. for($i = 0; $i < $this->numFiles; $i++)
  126. {
  127. $sEntry = $this->getNameIndex($i);
  128. //Use strpos() to check if the entry name contains the directory we want to extract
  129. if (strpos($sEntry, $sZipDir) === 0)
  130. {
  131. //Add the entry to our array if it in in our desired directory
  132. $aFiles[] = $sEntry;
  133. }
  134. }
  135. // Extract only the selected files
  136. if ((count($aFiles) > 0) && ($this->extractTo($sDestinationDir, $aFiles) === true))
  137. {
  138. return true;
  139. }
  140. return false;
  141. }
  142. } // class ZipArchiveEx
  143. class BackupException extends Exception
  144. {
  145. }
  146. class DBBackup
  147. {
  148. // To be overriden depending on the expected usages
  149. protected function LogInfo($sMsg)
  150. {
  151. }
  152. protected function LogError($sMsg)
  153. {
  154. }
  155. protected $sDBHost;
  156. protected $iDBPort;
  157. protected $sDBUser;
  158. protected $sDBPwd;
  159. protected $sDBName;
  160. protected $sDBSubName;
  161. /**
  162. * Connects to the database to backup
  163. * By default, connects to the current MetaModel (must be loaded)
  164. *
  165. * @param sDBHost string Database host server
  166. * @param $sDBUser string User login
  167. * @param $sDBPwd string User password
  168. * @param $sDBName string Database name
  169. * @param $sDBSubName string Prefix to the tables of itop in the database
  170. */
  171. public function __construct($sDBHost = null, $sDBUser = null, $sDBPwd = null, $sDBName = null, $sDBSubName = null)
  172. {
  173. if (is_null($sDBHost))
  174. {
  175. // Defaulting to the current config
  176. $sDBHost = MetaModel::GetConfig()->GetDBHost();
  177. $sDBUser = MetaModel::GetConfig()->GetDBUser();
  178. $sDBPwd = MetaModel::GetConfig()->GetDBPwd();
  179. $sDBName = MetaModel::GetConfig()->GetDBName();
  180. $sDBSubName = MetaModel::GetConfig()->GetDBSubName();
  181. }
  182. // Compute the port (if present in the host name)
  183. $aConnectInfo = explode(':', $sDBHost);
  184. $sDBHostName = $aConnectInfo[0];
  185. if (count($aConnectInfo) > 1)
  186. {
  187. $iDBPort = $aConnectInfo[1];
  188. }
  189. else
  190. {
  191. $iDBPort = null;
  192. }
  193. $this->sDBHost = $sDBHostName;
  194. $this->iDBPort = $iDBPort;
  195. $this->sDBUser = $sDBUser;
  196. $this->sDBPwd = $sDBPwd;
  197. $this->sDBName = $sDBName;
  198. $this->sDBSubName = $sDBSubName;
  199. }
  200. protected $sMySQLBinDir = '';
  201. /**
  202. * Create a normalized backup name, depending on the current date/time and Database
  203. * @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs
  204. */
  205. public function SetMySQLBinDir($sMySQLBinDir)
  206. {
  207. $this->sMySQLBinDir = $sMySQLBinDir;
  208. }
  209. /**
  210. * Create a normalized backup name, depending on the current date/time and Database
  211. * @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs
  212. */
  213. public function MakeName($sNameSpec = "__DB__-%Y-%m-%d")
  214. {
  215. $sFileName = $sNameSpec;
  216. $sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName);
  217. $sFileName = str_replace('__DB__', $this->sDBName, $sFileName);
  218. $sFileName = str_replace('__SUBNAME__', $this->sDBSubName, $sFileName);
  219. // Transform %Y, etc.
  220. $sFileName = strftime($sFileName);
  221. return $sFileName;
  222. }
  223. /**
  224. * @deprecated 2.4.0 Zip files are limited to 4 Gb, use CreateCompressedBackup to create tar.gz files
  225. * @param $sZipFile
  226. * @param null $sSourceConfigFile
  227. */
  228. public function CreateZip($sZipFile, $sSourceConfigFile = null)
  229. {
  230. $aContents = array();
  231. // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
  232. // (delete it before spawning a process)
  233. $sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
  234. $this->LogInfo("Data file: '$sDataFile'");
  235. $this->DoBackup($sDataFile);
  236. $aContents[] = array(
  237. 'source' => $sDataFile,
  238. 'dest' => 'itop-dump.sql',
  239. );
  240. foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile)
  241. {
  242. $aContents[] = array(
  243. 'source' => $sSourceFile,
  244. 'dest' => $sArchiveFile,
  245. );
  246. }
  247. $this->DoZip($aContents, $sZipFile);
  248. // Windows/IIS: the data file has been created by the spawned process...
  249. // trying to delete it will issue a warning, itself stopping the setup abruptely
  250. @unlink($sDataFile);
  251. }
  252. /**
  253. * @param string $sTargetFile Path and name, without the extension
  254. * @param string|null $sSourceConfigFile Configuration file to embed into the backup, if not the current one
  255. */
  256. public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null)
  257. {
  258. $this->LogInfo("Creating backup: '$sTargetFile.tar.gz'");
  259. // Note: PharData::compress strips everything after the first dot found in the name of the tar, then it adds .tar.gz
  260. // Hence, we have to create our own file in the target directory, and rename it when the process is complete
  261. $sTarFile = dirname($sTargetFile) . '/' . str_replace('.', '_', basename($sTargetFile)) . '.tar';
  262. $this->LogInfo("Tar file: '$sTarFile'");
  263. $oArchive = new PharData($sTarFile);
  264. // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
  265. // (delete it before spawning a process)
  266. // Note: the file is created by tempnam and might not be writeable by another process (Windows/IIS)
  267. // (delete it before spawning a process)
  268. $sDataFile = tempnam(SetupUtils::GetTmpDir(), 'itop-');
  269. $this->LogInfo("Data file: '$sDataFile'");
  270. $this->DoBackup($sDataFile);
  271. $oArchive->addFile($sDataFile, 'itop-dump.sql');
  272. // todo: reduce disk space needed by the operation by piping the output of mysqldump directly into the tar
  273. // tip1 : this syntax works fine (did not work with addFile)
  274. //$oArchive->buildFromIterator(
  275. // new ArrayIterator(
  276. // array('production.delta.xml' => fopen(ROOTDIR.'production.delta.xml', 'rb'))
  277. // )
  278. //);
  279. // tip2 : use the phar stream by redirecting the output of mysqldump into
  280. // phar://var/www/itop/data/backups/manual/trunk_pro-2017-07-05_15_10.tar.gz/itop-dump.sql
  281. //
  282. // new ArrayIterator(
  283. // array('production.delta.xml' => fopen(ROOTDIR.'production.delta.xml', 'rb'))
  284. // )
  285. //);
  286. // Windows/IIS: the data file has been created by the spawned process...
  287. // trying to delete it will issue a warning, itself stopping the setup abruptely
  288. @unlink($sDataFile);
  289. foreach ($this->GetAdditionalFiles($sSourceConfigFile) as $sArchiveFile => $sSourceFile)
  290. {
  291. if (is_dir($sSourceFile))
  292. {
  293. $this->LogInfo("Adding directory into tar file: '$sSourceFile', recorded as '$sArchiveFile'");
  294. // Note: Phar::buildFromDirectory does not allow to specify a destination subdirectory
  295. // Hence we have to add all files one by one
  296. $sSourceDir = realpath($sSourceFile);
  297. $sArchiveDir = trim($sArchiveFile, '/');
  298. $oDirectoryIterator = new RecursiveDirectoryIterator($sSourceDir, RecursiveDirectoryIterator::SKIP_DOTS);
  299. $oAllFiles = new RecursiveIteratorIterator($oDirectoryIterator);
  300. foreach ($oAllFiles as $oSomeFile)
  301. {
  302. if ($oSomeFile->isDir()) continue;
  303. // Replace the local path by the archive path - the resulting string starts with a '/'
  304. $sRelativePathName = substr($oSomeFile->getRealPath(), strlen($sSourceDir));
  305. // Under Windows realpath gives a mix of backslashes and slashes
  306. $sRelativePathName = str_replace('\\', '/', $sRelativePathName);
  307. $sArchiveFile = $sArchiveDir.$sRelativePathName;
  308. $oArchive->addFile($oSomeFile->getPathName(), $sArchiveFile);
  309. }
  310. }
  311. else
  312. {
  313. $this->LogInfo("Adding file into tar file: '$sSourceFile', recorded as '$sArchiveFile'");
  314. $oArchive->addFile($sSourceFile, $sArchiveFile);
  315. };
  316. }
  317. if (file_exists($sTarFile.'.gz'))
  318. {
  319. // Prevent the gzip compression from failing -> the whole operation is an overwrite
  320. $this->LogInfo("Overwriting tar.gz: '$sTarFile'");
  321. unlink($sTarFile.'.gz');
  322. }
  323. // zlib is a must!
  324. $oArchive->compress(Phar::GZ);
  325. // Cleanup
  326. unset($oArchive);
  327. unlink($sTarFile);
  328. if ($sTargetFile != $sTarFile)
  329. {
  330. // Give the file the expected name
  331. if (file_exists($sTargetFile.'.gz'))
  332. {
  333. // Remove it -> the whole operation is an overwrite
  334. $this->LogInfo("Overwriting tar.gz: '$sTargetFile'");
  335. unlink($sTargetFile.'.gz');
  336. }
  337. rename($sTarFile.'.gz', $sTargetFile.'.tar.gz');
  338. }
  339. }
  340. /**
  341. * List files to store into the archive, in addition to the SQL dump
  342. * @return array of sArchiveName => sFilePath
  343. */
  344. protected function GetAdditionalFiles($sSourceConfigFile)
  345. {
  346. $aRet = array();
  347. if (is_null($sSourceConfigFile))
  348. {
  349. $sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile();
  350. }
  351. if (!empty($sSourceConfigFile))
  352. {
  353. $aRet['config-itop.php'] = $sSourceConfigFile;
  354. }
  355. $sDeltaFile = APPROOT.'data/'.utils::GetCurrentEnvironment().'.delta.xml';
  356. if (file_exists($sDeltaFile))
  357. {
  358. $aRet['delta.xml'] = $sDeltaFile;
  359. }
  360. $sExtraDir = APPROOT.'data/'.utils::GetCurrentEnvironment().'-modules/';
  361. if (is_dir($sExtraDir))
  362. {
  363. $sModules = utils::GetCurrentEnvironment().'-modules/';
  364. $aRet[$sModules] = $sExtraDir;
  365. }
  366. return $aRet;
  367. }
  368. protected static function EscapeShellArg($sValue)
  369. {
  370. // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation
  371. // It suggests to rely on pctnl_* function instead of using escapeshellargs
  372. return escapeshellarg($sValue);
  373. }
  374. /**
  375. * Create a backup file
  376. */
  377. public function DoBackup($sBackupFileName)
  378. {
  379. $sHost = self::EscapeShellArg($this->sDBHost);
  380. $sUser = self::EscapeShellArg($this->sDBUser);
  381. $sPwd = self::EscapeShellArg($this->sDBPwd);
  382. $sDBName = self::EscapeShellArg($this->sDBName);
  383. // Just to check the connection to the DB (better than getting the retcode of mysqldump = 1)
  384. $oMysqli = $this->DBConnect();
  385. $sTables = '';
  386. if ($this->sDBSubName != '')
  387. {
  388. // This instance of iTop uses a prefix for the tables, so there may be other tables in the database
  389. // Let's explicitely list all the tables and views to dump
  390. $aTables = $this->EnumerateTables();
  391. if (count($aTables) == 0)
  392. {
  393. // No table has been found with the given prefix
  394. throw new BackupException("No table has been found with the given prefix");
  395. }
  396. $aEscapedTables = array();
  397. foreach($aTables as $sTable)
  398. {
  399. $aEscapedTables[] = self::EscapeShellArg($sTable);
  400. }
  401. $sTables = implode(' ', $aEscapedTables);
  402. }
  403. $this->LogInfo("Starting backup of $this->sDBHost/$this->sDBName(suffix:'$this->sDBSubName')");
  404. $sMySQLBinDir = utils::ReadParam('mysql_bindir', $this->sMySQLBinDir, true);
  405. if (empty($sMySQLBinDir))
  406. {
  407. $sMySQLDump = 'mysqldump';
  408. }
  409. else
  410. {
  411. $sMySQLDump = '"'.$sMySQLBinDir.'/mysqldump"';
  412. }
  413. // Store the results in a temporary file
  414. $sTmpFileName = self::EscapeShellArg($sBackupFileName);
  415. if (is_null($this->iDBPort))
  416. {
  417. $sPortOption = '';
  418. }
  419. else
  420. {
  421. $sPortOption = '--port='.$this->iDBPort.' ';
  422. }
  423. // Delete the file created by tempnam() so that the spawned process can write into it (Windows/IIS)
  424. unlink($sBackupFileName);
  425. // Note: opt implicitely sets lock-tables... which cancels the benefit of single-transaction!
  426. // skip-lock-tables compensates and allows for writes during a backup
  427. $sCommand = "$sMySQLDump --opt --skip-lock-tables --default-character-set=utf8 --add-drop-database --single-transaction --host=$sHost $sPortOption --user=$sUser --password=$sPwd --result-file=$sTmpFileName $sDBName $sTables 2>&1";
  428. $sCommandDisplay = "$sMySQLDump --opt --skip-lock-tables --default-character-set=utf8 --add-drop-database --single-transaction --host=$sHost $sPortOption --user=xxxxx --password=xxxxx --result-file=$sTmpFileName $sDBName $sTables";
  429. // Now run the command for real
  430. $this->LogInfo("Executing command: $sCommandDisplay");
  431. $aOutput = array();
  432. $iRetCode = 0;
  433. exec($sCommand, $aOutput, $iRetCode);
  434. foreach($aOutput as $sLine)
  435. {
  436. $this->LogInfo("mysqldump said: $sLine");
  437. }
  438. if ($iRetCode != 0)
  439. {
  440. // Cleanup residual output (Happens with Error 2020: Got packet bigger than 'maxallowedpacket' bytes...)
  441. if (file_exists($sBackupFileName))
  442. {
  443. unlink($sBackupFileName);
  444. }
  445. $this->LogError("Failed to execute: $sCommandDisplay. The command returned:$iRetCode");
  446. foreach($aOutput as $sLine)
  447. {
  448. $this->LogError("mysqldump said: $sLine");
  449. }
  450. if (count($aOutput) == 1)
  451. {
  452. $sMoreInfo = trim($aOutput[0]);
  453. }
  454. else
  455. {
  456. $sMoreInfo = "Check the log files '".realpath(APPROOT.'/log/setup.log or error.log')."' for more information.";
  457. }
  458. throw new BackupException("Failed to execute mysqldump: ".$sMoreInfo);
  459. }
  460. }
  461. /**
  462. * Helper to create a ZIP out of several files
  463. */
  464. protected function DoZip($aFiles, $sZipArchiveFile)
  465. {
  466. foreach ($aFiles as $aFile)
  467. {
  468. $sFile = $aFile['source'];
  469. if (!is_file($sFile) && !is_dir($sFile))
  470. {
  471. throw new BackupException("File '$sFile' does not exist or could not be read");
  472. }
  473. }
  474. // Make sure the target path exists
  475. $sZipDir = dirname($sZipArchiveFile);
  476. SetupUtils::builddir($sZipDir);
  477. $oZip = new ZipArchiveEx();
  478. $res = $oZip->open($sZipArchiveFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
  479. if ($res === TRUE)
  480. {
  481. foreach ($aFiles as $aFile)
  482. {
  483. if (is_dir($aFile['source']))
  484. {
  485. $oZip->addDir($aFile['source'], $aFile['dest']);
  486. }
  487. else
  488. {
  489. $oZip->addFile($aFile['source'], $aFile['dest']);
  490. }
  491. }
  492. if ($oZip->close())
  493. {
  494. $this->LogInfo("Archive: $sZipArchiveFile created");
  495. }
  496. else
  497. {
  498. $this->LogError("Failed to save zip archive: $sZipArchiveFile");
  499. throw new BackupException("Failed to save zip archive: $sZipArchiveFile");
  500. }
  501. }
  502. else
  503. {
  504. $this->LogError("Failed to create zip archive: $sZipArchiveFile.");
  505. throw new BackupException("Failed to create zip archive: $sZipArchiveFile.");
  506. }
  507. }
  508. /**
  509. * Helper to download the file directly from the browser
  510. */
  511. public function DownloadBackup($sFile)
  512. {
  513. header('Content-Description: File Transfer');
  514. header('Content-Type: multipart/x-zip');
  515. header('Content-Disposition: inline; filename="'.basename($sFile).'"');
  516. header('Expires: 0');
  517. header('Cache-Control: must-revalidate');
  518. header('Pragma: public');
  519. header('Content-Length: '.filesize($sFile));
  520. readfile($sFile);
  521. }
  522. /**
  523. * Helper to open a Database connection
  524. */
  525. protected function DBConnect()
  526. {
  527. if (is_null($this->iDBPort))
  528. {
  529. $oMysqli = new mysqli($this->sDBHost, $this->sDBUser, $this->sDBPwd);
  530. }
  531. else
  532. {
  533. $oMysqli = new mysqli($this->sDBHost, $this->sDBUser, $this->sDBPwd, '', $this->iDBPort);
  534. }
  535. if ($oMysqli->connect_errno)
  536. {
  537. $sHost = is_null($this->iDBPort) ? $this->sDBHost : $this->sDBHost.' on port '.$this->iDBPort;
  538. throw new BackupException("Cannot connect to the MySQL server '$this->sDBHost' (".$oMysqli->connect_errno . ") ".$oMysqli->connect_error);
  539. }
  540. if (!$oMysqli->select_db($this->sDBName))
  541. {
  542. throw new BackupException("The database '$this->sDBName' does not seem to exist");
  543. }
  544. return $oMysqli;
  545. }
  546. /**
  547. * Helper to enumerate the tables of the database
  548. */
  549. protected function EnumerateTables()
  550. {
  551. $oMysqli = $this->DBConnect();
  552. if ($this->sDBSubName != '')
  553. {
  554. $oResult = $oMysqli->query("SHOW TABLES LIKE '{$this->sDBSubName}%'");
  555. }
  556. else
  557. {
  558. $oResult = $oMysqli->query("SHOW TABLES");
  559. }
  560. if (!$oResult)
  561. {
  562. throw new BackupException("Failed to execute the SHOW TABLES query: ".$oMysqli->error);
  563. }
  564. $aTables = array();
  565. while ($aRow = $oResult->fetch_row())
  566. {
  567. $aTables[] = $aRow[0];
  568. }
  569. return $aTables;
  570. }
  571. }
  572. }
  573. class TarGzArchive implements BackupArchive
  574. {
  575. /*
  576. * @var PharData
  577. */
  578. protected $oPharArchive;
  579. /*
  580. * string[]
  581. */
  582. protected $aFiles = null;
  583. public function __construct($sFile)
  584. {
  585. $this->oPharArchive = new PharData($sFile);
  586. }
  587. /**
  588. * @param string $sFile
  589. * @return bool <b>TRUE</b> if the file is present, <b>FALSE</b> otherwise.
  590. */
  591. public function hasFile($sFile)
  592. {
  593. return $this->oPharArchive->offsetExists($sFile);
  594. }
  595. /**
  596. * @param string $sDirectory
  597. * @return bool <b>TRUE</b> if the directory is present, <b>FALSE</b> otherwise.
  598. */
  599. public function hasDir($sDirectory)
  600. {
  601. return $this->oPharArchive->offsetExists($sDirectory);
  602. }
  603. /**
  604. * @param string $sDestinationDir
  605. * @param string $sArchiveFile
  606. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  607. */
  608. public function extractFileTo($sDestinationDir, $sArchiveFile)
  609. {
  610. return $this->oPharArchive->extractTo($sDestinationDir, $sArchiveFile, true);
  611. }
  612. /**
  613. * Extract a whole directory from the archive.
  614. * Usage: $oArchive->extractDirTo('/var/www/html/itop/data', '/production-modules/')
  615. * @param string $sDestinationDir
  616. * @param string $sArchiveDir Note: must start and end with a slash !!
  617. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  618. */
  619. public function extractDirTo($sDestinationDir, $sArchiveDir)
  620. {
  621. $aFiles = array();
  622. foreach ($this->getFiles($sArchiveDir) as $oFileInfo)
  623. {
  624. $aFiles[] = $oFileInfo->getRelativePath();
  625. }
  626. if ((count($aFiles) > 0) && ($this->oPharArchive->extractTo($sDestinationDir, $aFiles, true) === true))
  627. {
  628. return true;
  629. }
  630. return false;
  631. }
  632. /**
  633. * Returns the entry contents using its name
  634. * @param string $name Name of the entry
  635. * @param int $length [optional] The length to be read from the entry. If 0, then the entire entry is read.
  636. * @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>
  637. * @return string the contents of the entry on success or <b>FALSE</b> on failure.
  638. */
  639. public function getFromName($name, $length = 0, $flags = null)
  640. {
  641. $oFileInfo = $this->oPharArchive->offsetGet($name);
  642. $sFile = $oFileInfo->getPathname();
  643. $sRet = file_get_contents($sFile);
  644. return $sRet;
  645. }
  646. /**
  647. * @param string|null $sArchivePath Path to search for
  648. * @return null
  649. */
  650. public function getFiles($sArchivePath = null)
  651. {
  652. if ($this->aFiles === null)
  653. {
  654. // Initial load
  655. $this->buildFileList();
  656. }
  657. if ($sArchivePath === null)
  658. {
  659. // Take them all
  660. $aRet = $this->aFiles;
  661. }
  662. else
  663. {
  664. // Filter out files not in the given path
  665. $aRet = array();
  666. foreach ($this->aFiles as $oFileInfo)
  667. {
  668. if ($oFileInfo->isUnder($sArchivePath))
  669. {
  670. $aRet[] = $oFileInfo;
  671. }
  672. }
  673. }
  674. return $aRet;
  675. }
  676. /**
  677. * @param PharData|null $oPharData
  678. * @param string $sArchivePath Path relatively to the archive root
  679. */
  680. protected function buildFileList($oPharData = null, $sArchivePath = '/')
  681. {
  682. if ($oPharData === null)
  683. {
  684. $oPharData = $this->oPharArchive;
  685. }
  686. foreach($oPharData as $oPharFileInfo)
  687. {
  688. if($oPharFileInfo->isDir())
  689. {
  690. $oSubDirectory = new PharData($oPharFileInfo->getPathname());
  691. // Recurse
  692. $this->buildFileList($oSubDirectory, $sArchivePath.'/'.$oPharFileInfo->getFileName());
  693. }
  694. else
  695. {
  696. $this->aFiles[] = new TarGzFileInfo($oPharFileInfo, $sArchivePath);
  697. }
  698. }
  699. }
  700. }
  701. class TarGzFileInfo
  702. {
  703. public function __construct(PharFileInfo $oFileInfo, $sArchivePath)
  704. {
  705. $this->oPharFileInfo = $oFileInfo;
  706. $this->sArchivePath = trim($sArchivePath, '/');
  707. }
  708. protected $sArchivePath;
  709. protected $oPharFileInfo;
  710. public function getPathname()
  711. {
  712. return $this->oPharFileInfo->getPathname();
  713. }
  714. public function getRelativePath()
  715. {
  716. return $this->sArchivePath.'/'.$this->oPharFileInfo->getFilename();
  717. }
  718. public function isUnder($sArchivePath)
  719. {
  720. $sTestedPath = trim($sArchivePath, '/');
  721. return (strpos($this->sArchivePath, $sTestedPath) === 0);
  722. }
  723. }