PageRenderTime 55ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/administrator/components/com_akeeba/akeeba/core/domain/finalization.php

https://bitbucket.org/kraymitchell/apex
PHP | 998 lines | 723 code | 117 blank | 158 comment | 149 complexity | 7b40a1da6c72d783295845484c8a55be MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, BSD-3-Clause, LGPL-2.1, GPL-3.0
  1. <?php
  2. /**
  3. * Akeeba Engine
  4. * The modular PHP5 site backup engine
  5. * @copyright Copyright (c)2009-2012 Nicholas K. Dionysopoulos
  6. * @license GNU GPL version 3 or, at your option, any later version
  7. * @package akeebaengine
  8. *
  9. */
  10. // Protection against direct access
  11. defined('AKEEBAENGINE') or die('Restricted access');
  12. /**
  13. * Backup finalization domain
  14. */
  15. class AECoreDomainFinalization extends AEAbstractPart
  16. {
  17. private $action_queue = array();
  18. private $action_handlers = array();
  19. private $current_method = '';
  20. private $backup_parts = array();
  21. private $backup_parts_index = -1;
  22. private $update_stats = false;
  23. private $remote_files_killlist = null;
  24. // Used for percentage reporting
  25. private $steps_total = 0;
  26. private $steps_done = 0;
  27. private $substeps_total = 0;
  28. private $substeps_done = 0;
  29. /**
  30. * Implements the abstract method
  31. * @see akeeba/abstract/AEAbstractPart#_prepare()
  32. */
  33. protected function _prepare()
  34. {
  35. // Make sure the break flag is not set
  36. $configuration = AEFactory::getConfiguration();
  37. $configuration->get('volatile.breakflag', false);
  38. // Populate actions queue
  39. $this->action_queue = array(
  40. 'remove_temp_files',
  41. 'update_statistics',
  42. 'update_filesizes',
  43. 'run_post_processing',
  44. 'apply_quotas',
  45. 'apply_remote_quotas',
  46. 'mail_administrators',
  47. );
  48. // Allow adding finalization action objects using the volatile.core.finalization.action_handlers array
  49. $customHandlers = $configuration->get('volatile.core.finalization.action_handlers', null);
  50. if(is_array($customHandlers) && !empty($customHandlers)) {
  51. foreach($customHandlers as $handler) {
  52. $this->action_handlers[] = $handler;
  53. }
  54. }
  55. // Do we have a custom action queue set in volatile.core.finalization.action_queue?
  56. $customQueue = $configuration->get('volatile.core.finalization.action_queue', null);
  57. if(is_array($customQueue) && !empty($customQueue)) {
  58. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Overriding finalization action queue');
  59. $this->action_queue = array();
  60. foreach($customQueue as $action) {
  61. if(method_exists($this, $action)) {
  62. $this->action_queue[] = $action;
  63. } else {
  64. foreach($this->action_handlers as $handler) {
  65. if(method_exists($handler, $action)) {
  66. $this->action_queue[] = $action;
  67. break;
  68. }
  69. }
  70. }
  71. }
  72. }
  73. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Finalization action queue: '.implode(', ',$this->action_queue));
  74. $this->steps_total = count($this->action_queue);
  75. $this->steps_done = 0;
  76. $this->substeps_total = 0;
  77. $this->substeps_done = 0;
  78. // Seed the method
  79. $this->current_method = array_shift($this->action_queue);
  80. // Set ourselves to running state
  81. $this->setState('running');
  82. }
  83. /**
  84. * Implements the abstract method
  85. * @see akeeba/abstract/AEAbstractPart#_run()
  86. */
  87. protected function _run()
  88. {
  89. $configuration = AEFactory::getConfiguration();
  90. if($this->getState() == 'postrun') return;
  91. $finished = (empty($this->action_queue)) && ($this->current_method == '');
  92. if($finished)
  93. {
  94. $this->setState('postrun');
  95. return;
  96. }
  97. $this->setState('running');
  98. $timer = AEFactory::getTimer();
  99. // Continue processing while we have still enough time and stuff to do
  100. while( ($timer->getTimeLeft() > 0) && (!$finished) && (!$configuration->get('volatile.breakflag', false)) )
  101. {
  102. $method = $this->current_method;
  103. if(method_exists($this, $method)) {
  104. $status = $this->$method();
  105. } else {
  106. $status = true;
  107. if(!empty($this->action_handlers)) foreach($this->action_handlers as $handler) {
  108. if(method_exists($handler, $method)) {
  109. $status = $handler->$method($this);
  110. break;
  111. }
  112. }
  113. }
  114. if($status === true)
  115. {
  116. $this->current_method = '';
  117. $this->steps_done++;
  118. $finished = (empty($this->action_queue));
  119. if(!$finished) {
  120. $this->current_method = array_shift($this->action_queue);
  121. $this->substeps_total = 0;
  122. $this->substeps_done = 0;
  123. }
  124. }
  125. }
  126. if($finished) {
  127. $this->setState('postrun');
  128. $this->setStep('');
  129. $this->setSubstep('');
  130. }
  131. }
  132. /**
  133. * Implements the abstract method
  134. * @see akeeba/abstract/AEAbstractPart#_finalize()
  135. */
  136. protected function _finalize()
  137. {
  138. $this->setState('finished');
  139. }
  140. /**
  141. * Sends an email to the administrators
  142. * @return bool
  143. */
  144. private function mail_administrators()
  145. {
  146. $this->setStep('Processing emails to administrators');
  147. $this->setSubstep('');
  148. // Skip email for back-end backups
  149. if(AEPlatform::getInstance()->get_backup_origin() == 'backend' ) return true;
  150. $must_email = AEPlatform::getInstance()->get_platform_configuration_option('frontend_email_on_finish', 0) != 0;
  151. if(!$must_email) return true;
  152. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Preparing to send e-mail to administrators");
  153. $email = AEPlatform::getInstance()->get_platform_configuration_option('frontend_email_address', '');
  154. $email = trim($email);
  155. if( !empty($email) )
  156. {
  157. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Using pre-defined list of emails");
  158. $emails = array($email);
  159. }
  160. else
  161. {
  162. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Fetching list of Super Administrator emails");
  163. $emails = AEPlatform::getInstance()->get_administrator_emails();
  164. }
  165. if(!empty($emails))
  166. {
  167. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Creating email subject and body");
  168. // Fetch user's preferences
  169. $subject = trim(AEPlatform::getInstance()->get_platform_configuration_option('frontend_email_subject',''));
  170. $body = trim(AEPlatform::getInstance()->get_platform_configuration_option('frontend_email_body',''));
  171. // Get the statistics
  172. $statistics = AEFactory::getStatistics();
  173. $stat = $statistics->getRecord();
  174. $parts = AEUtilStatistics::get_all_filenames($stat, false);
  175. $profile_number = AEPlatform::getInstance()->get_active_profile();
  176. $profile_name = AEPlatform::getInstance()->get_profile_name($profile_number);
  177. $parts = AEUtilStatistics::get_all_filenames($stat, false);
  178. $stat = (object)$stat;
  179. $num_parts = $stat->multipart;
  180. if($num_parts == 0) $num_parts = 1; // Non-split archives have a part count of 0
  181. $parts_list = '';
  182. if(!empty($parts)) foreach($parts as $file) {
  183. $parts_list .= "\t".basename($file)."\n";
  184. }
  185. // Get the remote storage status
  186. $remote_status = '';
  187. $post_proc_engine = AEFactory::getConfiguration()->get('akeeba.advanced.proc_engine');
  188. if(!empty($post_proc_engine) && ($post_proc_engine != 'none')) {
  189. if(empty($stat->remote_filename)) {
  190. $remote_status = AEPlatform::getInstance()->translate('COM_AKEEBA_EMAIL_POSTPROCESSING_FAILED');
  191. } else {
  192. $remote_status = AEPlatform::getInstance()->translate('COM_AKEEBA_EMAIL_POSTPROCESSING_SUCCESS');
  193. }
  194. }
  195. // Do we need a default subject?
  196. if(empty($subject)) {
  197. // Get the default subject
  198. $subject = AEPlatform::getInstance()->translate('EMAIL_SUBJECT_OK');
  199. } else {
  200. // Post-process the subject
  201. $subject = AEUtilFilesystem::replace_archive_name_variables($subject);
  202. }
  203. // Do we need a default body?
  204. if(empty($body)) {
  205. $body = AEPlatform::getInstance()->translate('EMAIL_BODY_OK');
  206. $info_source = AEPlatform::getInstance()->translate('EMAIL_BODY_INFO');
  207. $body .= "\n\n" . sprintf($info_source, $profile_number, $num_parts) . "\n\n";
  208. $body .= $parts_list;
  209. } else {
  210. // Post-process the body
  211. $body = AEUtilFilesystem::replace_archive_name_variables($body);
  212. $body = str_replace('[PROFILENUMBER]', $profile_number, $body);
  213. $body = str_replace('[PROFILENAME]', $profile_name, $body);
  214. $body = str_replace('[PARTCOUNT]', $num_parts, $body);
  215. $body = str_replace('[FILELIST]', $parts_list, $body);
  216. $body = str_replace('[REMOTESTATUS]', $remote_status, $body);
  217. }
  218. // Sometimes $body contains literal \n instead of newlines
  219. $body = str_replace('\\n',"\n", $body);
  220. foreach($emails as $email)
  221. {
  222. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Sending email to $email");
  223. AEPlatform::getInstance()->send_email($email, $subject, $body);
  224. }
  225. } else {
  226. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "No email recipients found! Skipping email.");
  227. }
  228. return true;
  229. }
  230. /**
  231. * Removes temporary files
  232. * @return bool
  233. */
  234. private function remove_temp_files()
  235. {
  236. $this->setStep('Removing temporary files');
  237. $this->setSubstep('');
  238. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Removing temporary files" );
  239. AEUtilTempfiles::deleteTempFiles();
  240. return true;
  241. }
  242. /**
  243. * Runs the writer's post-processing steps
  244. * @return bool
  245. */
  246. private function run_post_processing()
  247. {
  248. $this->setStep('Post-processing');
  249. // Do not run if the archive engine doesn't produce archives
  250. $configuration = AEFactory::getConfiguration();
  251. $this->setSubstep('');
  252. $engine_name = $configuration->get('akeeba.advanced.proc_engine');
  253. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,"Loading post-processing engine object ($engine_name)");
  254. $post_proc = AEFactory::getPostprocEngine($engine_name);
  255. // Initialize the archive part list if required
  256. if(empty($this->backup_parts))
  257. {
  258. AEUtilLogger::WriteLog(_AE_LOG_INFO,'Initializing post-processing engine');
  259. // Initialize the flag for multistep post-processing of parts
  260. $configuration->set('volatile.postproc.filename', null);
  261. $configuration->set('volatile.postproc.directory', null);
  262. // Populate array w/ absolute names of backup parts
  263. $statistics = AEFactory::getStatistics();
  264. $stat = $statistics->getRecord();
  265. $this->backup_parts = AEUtilStatistics::get_all_filenames($stat, false);
  266. if(is_null($this->backup_parts)) {
  267. // No archive produced, or they are all already post-processed
  268. AEUtilLogger::WriteLog(_AE_LOG_INFO,'No archive files found to post-process');
  269. return true;
  270. }
  271. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, count($this->backup_parts).' files to process found');
  272. $this->substeps_total = count($this->backup_parts);
  273. $this->substeps_done = 0;
  274. $this->backup_parts_index = 0;
  275. // If we have an empty array, do not run
  276. if(empty($this->backup_parts)) return true;
  277. // Break step before processing?
  278. if($post_proc->break_before && !AEFactory::getConfiguration()->get('akeeba.tuning.nobreak.finalization',0))
  279. {
  280. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Breaking step before post-processing run');
  281. $configuration->set('volatile.breakflag', true);
  282. return false;
  283. }
  284. }
  285. // Make sure we don't accidentally break the step when not required to do so
  286. $configuration->set('volatile.breakflag', false);
  287. // Do we have a filename from the previous run of the post-proc engine?
  288. $filename = $configuration->get('volatile.postproc.filename', null);
  289. if(empty($filename)) {
  290. $filename = $this->backup_parts[$this->backup_parts_index];
  291. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Beginning post processing file '.$filename);
  292. } else {
  293. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Continuing post processing file '.$filename);
  294. }
  295. $this->setStep('Post-processing');
  296. $this->setSubstep( basename($filename) );
  297. $timer = AEFactory::getTimer();
  298. $startTime = $timer->getRunningTime();
  299. $result = $post_proc->processPart( $filename );
  300. $this->propagateFromObject($post_proc);
  301. if($result === false) {
  302. AEUtilLogger::WriteLog(_AE_LOG_WARNING, 'Failed to process file '.$filename);
  303. AEUtilLogger::WriteLog(_AE_LOG_WARNING, 'Error received from the post-processing engine:');
  304. AEUtilLogger::WriteLog(_AE_LOG_WARNING, implode("\n", $this->getWarnings()) );
  305. $this->setWarning('Failed to process file '.$filename);
  306. } elseif( $result === true ) {
  307. // The post-processing of this file ended successfully
  308. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'Finished post-processing file '.$filename);
  309. $configuration->set('volatile.postproc.filename', null);
  310. } else {
  311. // More work required
  312. AEUtilLogger::WriteLog(_AE_LOG_INFO, 'More post-processing steps required for file '.$filename);
  313. $configuration->set('volatile.postproc.filename', $filename);
  314. // Do we need to break the step?
  315. $endTime = $timer->getRunningTime();
  316. $stepTime = $endTime - $startTime;
  317. $timeLeft = $timer->getTimeLeft();
  318. if($timeLeft < $stepTime) {
  319. // We predict that running yet another step would cause a timeout
  320. $configuration->set('volatile.breakflag', true);
  321. } else {
  322. // We have enough time to run yet another step
  323. $configuration->set('volatile.breakflag', false);
  324. }
  325. }
  326. // Should we delete the file afterwards?
  327. if(
  328. $configuration->get('engine.postproc.common.delete_after',false)
  329. && $post_proc->allow_deletes
  330. && ($result === true)
  331. )
  332. {
  333. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Deleting already processed file '.$filename);
  334. AEPlatform::getInstance()->unlink($filename);
  335. } else {
  336. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, 'Not removing processed file '.$filename);
  337. }
  338. if($result === true) {
  339. // Move the index forward if the part finished processing
  340. $this->backup_parts_index++;
  341. // Mark substep done
  342. $this->substeps_done++;
  343. // Break step after processing?
  344. if($post_proc->break_after && !AEFactory::getConfiguration()->get('akeeba.tuning.nobreak.finalization',0)) $configuration->set('volatile.breakflag', true);
  345. // If we just finished processing the first archive part, save its remote path in the statistics.
  346. if(($this->substeps_done == 1) || ($this->substeps_total == 0)) {
  347. if(!empty($post_proc->remote_path))
  348. {
  349. $statistics = AEFactory::getStatistics();
  350. $remote_filename = $engine_name.'://';
  351. $remote_filename .= $post_proc->remote_path;
  352. $data = array(
  353. 'remote_filename' => $remote_filename
  354. );
  355. $remove_after = $configuration->get('engine.postproc.common.delete_after',false);
  356. if($remove_after) {
  357. $data['filesexist'] = 0;
  358. }
  359. $statistics->setStatistics($data);
  360. $this->propagateFromObject($statistics);
  361. }
  362. }
  363. // Are we past the end of the array (i.e. we're finished)?
  364. if( $this->backup_parts_index >= count($this->backup_parts) )
  365. {
  366. AEUtilLogger::WriteLog(_AE_LOG_INFO,'Post-processing has finished for all files');
  367. return true;
  368. }
  369. } elseif($result === false) {
  370. // If the post-processing failed, make sure we don't process anything else
  371. $this->backup_parts_index = count($this->backup_parts);
  372. $this->setWarning('Post-processing interrupted -- no more files will be transferred');
  373. return true;
  374. }
  375. // Indicate we're not done yet
  376. return false;
  377. }
  378. /**
  379. * Updates the backup statistics record
  380. * @return bool
  381. */
  382. private function update_statistics()
  383. {
  384. $this->setStep('Updating statistics');
  385. $this->setSubstep('');
  386. // Force a step break before updating stats (works around MySQL gone away issues)
  387. // 3.2.5 : Added conditional break logic after the call to setStatistics()
  388. /**
  389. if(!$this->update_stats)
  390. {
  391. $this->update_stats = true;
  392. $configuration = AEFactory::getConfiguration();
  393. $configuration->set('volatile.breakflag', true);
  394. return false;
  395. }
  396. /**/
  397. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Updating statistics" );
  398. // We finished normally. Fetch the stats record
  399. $statistics = AEFactory::getStatistics();
  400. $registry = AEFactory::getConfiguration();
  401. $data = array(
  402. 'backupend' => AEPlatform::getInstance()->get_timestamp_database(),
  403. 'status' => 'complete',
  404. 'multipart' => $registry->get('volatile.statistics.multipart', 0)
  405. );
  406. $result = $statistics->setStatistics($data);
  407. if($result === false) {
  408. // Most likely a "MySQL has gone away" issue...
  409. $this->update_stats = true;
  410. $configuration = AEFactory::getConfiguration();
  411. $configuration->set('volatile.breakflag', true);
  412. return false;
  413. }
  414. $this->propagateFromObject($statistics);
  415. $stat = (object)$statistics->getRecord();
  416. AEPlatform::getInstance()->remove_duplicate_backup_records($stat->archivename);
  417. return true;
  418. }
  419. private function update_filesizes()
  420. {
  421. $this->setStep('Updating file sizes');
  422. $this->setSubstep('');
  423. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Updating statistics with file sizes" );
  424. // Fetch the stats record
  425. $statistics = AEFactory::getStatistics();
  426. $record = $statistics->getRecord();
  427. $filenames = $statistics->get_all_filenames($record);
  428. $filesize = 0.0;
  429. if(!empty($filenames)) foreach($filenames as $file)
  430. {
  431. $size = @filesize($file);
  432. if($size !== false) $filesize += $size * 1.0;
  433. }
  434. $data = array(
  435. 'total_size' => $filesize
  436. );
  437. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Total size of backup archive (in bytes): $filesize" );
  438. $statistics->setStatistics($data);
  439. $this->propagateFromObject($statistics);
  440. return true;
  441. }
  442. /**
  443. * Applies the size and count quotas
  444. * @return bool
  445. */
  446. private function apply_quotas()
  447. {
  448. $this->setStep('Applying quotas');
  449. $this->setSubstep('');
  450. // If no quota settings are enabled, quit
  451. $registry = AEFactory::getConfiguration();
  452. $useDayQuotas = $registry->get('akeeba.quota.maxage.enable');
  453. $useCountQuotas = $registry->get('akeeba.quota.enable_count_quota');
  454. $useSizeQuotas = $registry->get('akeeba.quota.enable_size_quota');
  455. if(! ($useDayQuotas || $useCountQuotas || $useSizeQuotas) )
  456. {
  457. $this->apply_obsolete_quotas();
  458. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "No quotas were defined; old backup files will be kept intact" );
  459. return true; // No quota limits were requested
  460. }
  461. // Try to find the files to be deleted due to quota settings
  462. $statistics = AEFactory::getStatistics();
  463. $latestBackupId = $statistics->getId();
  464. // Get quota values
  465. $countQuota = $registry->get('akeeba.quota.count_quota');
  466. $sizeQuota = $registry->get('akeeba.quota.size_quota');
  467. $daysQuota = $registry->get('akeeba.quota.maxage.maxdays');
  468. $preserveDay = $registry->get('akeeba.quota.maxage.keepday');
  469. // Get valid-looking backup ID's
  470. $validIDs = AEPlatform::getInstance()->get_valid_backup_records(true, array('NOT','restorepoint'));
  471. // Create a list of valid files
  472. $allFiles = array();
  473. if(count($validIDs))
  474. {
  475. foreach($validIDs as $id)
  476. {
  477. $stat = AEPlatform::getInstance()->get_statistics($id);
  478. try {
  479. $backupstart = new DateTime($stat['backupstart']);
  480. $backupTS = $backupstart->format('U');
  481. $backupDay = $backupstart->format('d');
  482. } catch (Exception $e) {
  483. $backupTS = 0;
  484. $backupDay = 0;
  485. }
  486. // Multipart processing
  487. $filenames = AEUtilStatistics::get_all_filenames($stat, true);
  488. if(!is_null($filenames))
  489. {
  490. // Only process existing files
  491. $filesize = 0;
  492. foreach($filenames as $filename)
  493. {
  494. $filesize += @filesize($filename);
  495. }
  496. $allFiles[] = array('id' => $id, 'filenames' => $filenames, 'size' => $filesize, 'backupstart' => $backupTS, 'day' => $backupDay);
  497. }
  498. }
  499. }
  500. unset($validIDs);
  501. // If there are no files, exit early
  502. if(count($allFiles) == 0)
  503. {
  504. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "There were no old backup files to apply quotas on" );
  505. return true;
  506. }
  507. // Init arrays
  508. $killids = array();
  509. $ret = array();
  510. $leftover = array();
  511. // Do we need to apply maximum backup age quotas?
  512. if($useDayQuotas) {
  513. $killDatetime = new DateTime();
  514. $killDatetime->modify('-'.$daysQuota.($daysQuota == 1 ? ' day' : ' days'));
  515. $killTS = $killDatetime->format('U');
  516. foreach($allFiles as $file) {
  517. if($file['id'] == $latestBackupId) continue;
  518. // Is this on a preserve day?
  519. if($preserveDay > 0) {
  520. if($preserveDay == $file['day']) {
  521. $leftover[] = $file;
  522. continue;
  523. }
  524. }
  525. // Otherwise, check the timestamp
  526. if($file['backupstart'] < $killTS) {
  527. $ret[] = $file['filenames'];
  528. $killids[] = $file['id'];
  529. } else {
  530. $leftover[] = $file;
  531. }
  532. }
  533. }
  534. // Do we need to apply count quotas?
  535. if($useCountQuotas && is_numeric($countQuota) && !($countQuota <= 0) && !$useDayQuotas )
  536. {
  537. // Are there more files than the quota limit?
  538. if( !(count($allFiles) > $countQuota) )
  539. {
  540. // No, effectively skip the quota checking
  541. $leftover = $allFiles;
  542. }
  543. else
  544. {
  545. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Processing count quotas" );
  546. // Yes, aply the quota setting. Add to $ret all entries minus the last
  547. // $countQuota ones.
  548. $totalRecords = count($allFiles);
  549. $checkLimit = $totalRecords - $countQuota;
  550. // Only process if at least one file (current backup!) is to be left
  551. for($count = 0; $count < $totalRecords; $count++)
  552. {
  553. $def = array_pop($allFiles);
  554. if($def['id'] == $latestBackupId) {
  555. array_push($allFiles, $def);
  556. continue;
  557. }
  558. if(count($ret) < $checkLimit)
  559. {
  560. if($latestBackupId != $def['id']) {
  561. $ret[] = $def['filenames'];
  562. $killids[] = $def['id'];
  563. }
  564. }
  565. else
  566. {
  567. $leftover[] = $def;
  568. }
  569. }
  570. unset($allFiles);
  571. }
  572. }
  573. else
  574. {
  575. // No count quotas are applied
  576. $leftover = $allFiles;
  577. }
  578. // Do we need to apply size quotas?
  579. if( $useSizeQuotas && is_numeric($sizeQuota) && !($sizeQuota <= 0) && (count($leftover) > 0) && !$useDayQuotas )
  580. {
  581. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Processing size quotas" );
  582. // OK, let's start counting bytes!
  583. $runningSize = 0;
  584. while(count($leftover) > 0)
  585. {
  586. // Each time, remove the last element of the backup array and calculate
  587. // running size. If it's over the limit, add the archive to the return array.
  588. $def = array_pop($leftover);
  589. $runningSize += $def['size'];
  590. if($runningSize >= $sizeQuota)
  591. {
  592. if($latestBackupId == $def['id'])
  593. {
  594. $runningSize -= $def['size'];
  595. }
  596. else
  597. {
  598. $ret[] = $def['filenames'];
  599. $killids[] = $def['filenames'];
  600. }
  601. }
  602. }
  603. }
  604. // Convert the $ret 2-dimensional array to single dimensional
  605. $quotaFiles = array();
  606. foreach($ret as $temp)
  607. {
  608. foreach($temp as $filename)
  609. {
  610. $quotaFiles[] = $filename;
  611. }
  612. }
  613. // Update the statistics record with the removed remote files
  614. if(!empty($killids)) foreach($killids as $id) {
  615. $data = array('filesexist' => '0');
  616. AEPlatform::getInstance()->set_or_update_statistics($id, $data, $this);
  617. }
  618. // Apply quotas
  619. if(count($quotaFiles) > 0)
  620. {
  621. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Applying quotas" );
  622. jimport('joomla.filesystem.file');
  623. foreach($quotaFiles as $file)
  624. {
  625. if(!@AEPlatform::getInstance()->unlink($file))
  626. {
  627. $this->setWarning("Failed to remove old backup file ".$file );
  628. }
  629. }
  630. }
  631. $this->apply_obsolete_quotas();
  632. return true;
  633. }
  634. private function apply_remote_quotas()
  635. {
  636. $this->setStep('Applying remote storage quotas');
  637. $this->setSubstep('');
  638. // Make sure we are enabled
  639. $config = AEFactory::getConfiguration();
  640. $enableRemote = $config->get('akeeba.quota.remote',0);
  641. if(!$enableRemote) return true;
  642. // Get the list of files to kill
  643. if(empty($this->remote_files_killlist)) {
  644. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,'Applying remote file quotas');
  645. $this->remote_files_killlist = $this->get_remote_quotas();
  646. if(empty($this->remote_files_killlist)) {
  647. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,'No remote files to apply quotas to were found');
  648. return true;
  649. }
  650. }
  651. // Remove the files
  652. $timer = AEFactory::getTimer();
  653. while($timer->getRunningTime() && count($this->remote_files_killlist))
  654. {
  655. $filename = array_shift($this->remote_files_killlist);
  656. list($engineName, $path) = explode('://',$filename);
  657. $engine = AEFactory::getPostprocEngine($engineName);
  658. if(!$engine->can_delete) continue;
  659. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,"Removing $filename");
  660. $result = $engine->delete($path);
  661. if(!$result) {
  662. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,"Removal failed: ".$engine->getWarning());
  663. }
  664. }
  665. // Return false if we have more work to do or true if we're done
  666. if(count($this->remote_files_killlist)) {
  667. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,"Remote file removal will continue in the next step");
  668. return false;
  669. } else {
  670. AEUtilLogger::WriteLog(_AE_LOG_DEBUG,"Remote file quotas applied successfully");
  671. return true;
  672. }
  673. }
  674. /**
  675. * Applies the size and count quotas
  676. * @return bool
  677. */
  678. private function get_remote_quotas()
  679. {
  680. // Get all records with a remote filename
  681. $allRecords = AEPlatform::getInstance()->get_valid_remote_records();
  682. // Bail out if no records found
  683. if(empty($allRecords)) return array();
  684. // Try to find the files to be deleted due to quota settings
  685. $statistics = AEFactory::getStatistics();
  686. $latestBackupId = $statistics->getId();
  687. // Filter out the current record
  688. $temp = array();
  689. foreach($allRecords as $item)
  690. {
  691. if($item['id'] == $latestBackupId) continue;
  692. $item['files'] = $this->get_remote_files($item['remote_filename'], $item['multipart']);
  693. $temp[] = $item;
  694. }
  695. $allRecords = $temp;
  696. // Bail out if only the current backup was included in the list
  697. if(count($allRecords) == 0) return array();
  698. // Get quota values
  699. $registry = AEFactory::getConfiguration();
  700. $countQuota = $registry->get('akeeba.quota.count_quota');
  701. $sizeQuota = $registry->get('akeeba.quota.size_quota');
  702. $useCountQuotas = $registry->get('akeeba.quota.enable_count_quota');
  703. $useSizeQuotas = $registry->get('akeeba.quota.enable_size_quota');
  704. $useDayQuotas = $registry->get('akeeba.quota.maxage.enable');
  705. $daysQuota = $registry->get('akeeba.quota.maxage.maxdays');
  706. $preserveDay = $registry->get('akeeba.quota.maxage.keepday');
  707. $leftover = array();
  708. $ret = array();
  709. $killids = array();
  710. if($useDayQuotas) {
  711. $killDatetime = new DateTime();
  712. $killDatetime->modify('-'.$daysQuota.($daysQuota == 1 ? ' day' : ' days'));
  713. $killTS = $killDatetime->format('U');
  714. foreach($allRecords as $def) {
  715. $backupstart = new DateTime($def['backupstart']);
  716. $backupTS = $backupstart->format('U');
  717. $backupDay = $backupstart->format('d');
  718. // Is this on a preserve day?
  719. if($preserveDay > 0) {
  720. if($preserveDay == $backupDay) {
  721. $leftover[] = $def;
  722. continue;
  723. }
  724. }
  725. // Otherwise, check the timestamp
  726. if($backupTS < $killTS) {
  727. $ret[] = $def['files'];
  728. $killids[] = $def['id'];
  729. } else {
  730. $leftover[] = $def;
  731. }
  732. }
  733. }
  734. // Do we need to apply count quotas?
  735. if($useCountQuotas && ($countQuota >= 1) && !$useDayQuotas )
  736. {
  737. $countQuota--;
  738. // Are there more files than the quota limit?
  739. if( !(count($allRecords) > $countQuota) )
  740. {
  741. // No, effectively skip the quota checking
  742. $leftover = $allRecords;
  743. }
  744. else
  745. {
  746. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Processing remote count quotas" );
  747. // Yes, apply the quota setting.
  748. $totalRecords = count($allRecords);
  749. for($count = 0; $count <= $totalRecords; $count++)
  750. {
  751. $def = array_pop($allRecords);
  752. if(count($leftover) >= $countQuota)
  753. {
  754. $ret[] = $def['files'];
  755. $killids[] = $def['id'];
  756. }
  757. else
  758. {
  759. $leftover[] = $def;
  760. }
  761. }
  762. unset($allRecords);
  763. }
  764. }
  765. else
  766. {
  767. // No count quotas are applied
  768. $leftover = $allRecords;
  769. }
  770. // Do we need to apply size quotas?
  771. if( $useSizeQuotas && ($sizeQuota > 0) && (count($leftover) > 0) && !$useDayQuotas )
  772. {
  773. AEUtilLogger::WriteLog(_AE_LOG_DEBUG, "Processing remote size quotas" );
  774. // OK, let's start counting bytes!
  775. $runningSize = 0;
  776. while(count($leftover) > 0)
  777. {
  778. // Each time, remove the last element of the backup array and calculate
  779. // running size. If it's over the limit, add the archive to the $ret array.
  780. $def = array_pop($leftover);
  781. $runningSize += $def['total_size'];
  782. if($runningSize >= $sizeQuota)
  783. {
  784. $ret[] = $def['files'];
  785. $killids[] = $def['id'];
  786. }
  787. }
  788. }
  789. // Convert the $ret 2-dimensional array to single dimensional
  790. $quotaFiles = array();
  791. foreach($ret as $temp)
  792. {
  793. if(!is_array($temp) || empty($temp)) continue;
  794. foreach($temp as $filename)
  795. {
  796. $quotaFiles[] = $filename;
  797. }
  798. }
  799. // Update the statistics record with the removed remote files
  800. if(!empty($killids)) foreach($killids as $id) {
  801. if(empty($id)) continue;
  802. $data = array('remote_filename' => '');
  803. AEPlatform::getInstance()->set_or_update_statistics($id, $data, $this);
  804. }
  805. return $quotaFiles;
  806. }
  807. private function get_remote_files($filename, $multipart)
  808. {
  809. $result = array();
  810. $extension = substr($filename, -3);
  811. $base = substr($filename, 0, -4);
  812. $result[] = $filename;
  813. if($multipart > 1) {
  814. for($i = 1; $i < $multipart; $i++)
  815. {
  816. $newExt = substr($extension,0,1).sprintf('%02u',$i);
  817. $result[] = $base.'.'.$newExt;
  818. }
  819. }
  820. return $result;
  821. }
  822. /**
  823. * Keeps a maximum number of "obsolete" records
  824. */
  825. private function apply_obsolete_quotas()
  826. {
  827. $this->setStep('Applying quota limit on obsolete backup records');
  828. $this->setSubstep('');
  829. $registry = AEFactory::getConfiguration();
  830. $limit = $registry->get('akeeba.quota.obsolete_quota', 0);
  831. $limit = (int)$limit;
  832. if($limit <= 0) return;
  833. $statsTable = AEPlatform::getInstance()->tableNameStats;
  834. $db = AEFactory::getDatabase( AEPlatform::getInstance()->get_platform_database_options() );
  835. $query = $db->getQuery(true)
  836. ->select($db->qn('id'))
  837. ->from($db->qn($statsTable))
  838. ->where($db->qn('status').' = '.$db->q('complete'))
  839. ->where($db->qn('filesexist').'='.$db->q('0'))
  840. ->order($db->qn('id').' DESC');
  841. $db->setQuery($query, $limit, 100000);
  842. if(version_compare(JVERSION, '3.0', 'ge')) {
  843. $array = $db->loadColumn();
  844. } else {
  845. $array = $db->loadResultArray();
  846. }
  847. if(empty($array)) return;
  848. $ids = array();
  849. foreach($array as $id) {
  850. $ids[] = $db->q($id);
  851. }
  852. $ids = implode(',', $ids);
  853. $query = $db->getQuery(true)
  854. ->delete($db->qn($statsTable))
  855. ->where($db->qn('id')." IN ($ids)");
  856. $db->setQuery($query);
  857. $db->query();
  858. }
  859. /**
  860. * Get the percentage of finalization steps done
  861. * @see backend/akeeba/abstract/AEAbstractPart#getProgress()
  862. */
  863. public function getProgress()
  864. {
  865. if($this->steps_total <= 0) return 0;
  866. $overall = $this->steps_done / $this->steps_total;
  867. $local = 0;
  868. if($this->substeps_total > 0) {
  869. $local = $this->substeps_done / $this->substeps_total;
  870. }
  871. return $overall + ($local / $this->steps_total);
  872. }
  873. public function relayStep($step)
  874. {
  875. $this->setStep($step);
  876. }
  877. public function relaySubstep($substep)
  878. {
  879. $this->setSubstep($substep);
  880. }
  881. }