[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 3 // This file is part of Moodle - http://moodle.org/ 4 // 5 // Moodle is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // Moodle is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 17 18 /** 19 * Utility helper for automated backups run through cron. 20 * 21 * @package core 22 * @subpackage backup 23 * @copyright 2010 Sam Hemelryk 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * This class is an abstract class with methods that can be called to aid the 31 * running of automated backups over cron. 32 */ 33 abstract class backup_cron_automated_helper { 34 35 /** Automated backups are active and ready to run */ 36 const STATE_OK = 0; 37 /** Automated backups are disabled and will not be run */ 38 const STATE_DISABLED = 1; 39 /** Automated backups are all ready running! */ 40 const STATE_RUNNING = 2; 41 42 /** Course automated backup completed successfully */ 43 const BACKUP_STATUS_OK = 1; 44 /** Course automated backup errored */ 45 const BACKUP_STATUS_ERROR = 0; 46 /** Course automated backup never finished */ 47 const BACKUP_STATUS_UNFINISHED = 2; 48 /** Course automated backup was skipped */ 49 const BACKUP_STATUS_SKIPPED = 3; 50 /** Course automated backup had warnings */ 51 const BACKUP_STATUS_WARNING = 4; 52 /** Course automated backup has yet to be run */ 53 const BACKUP_STATUS_NOTYETRUN = 5; 54 55 /** Run if required by the schedule set in config. Default. **/ 56 const RUN_ON_SCHEDULE = 0; 57 /** Run immediately. **/ 58 const RUN_IMMEDIATELY = 1; 59 60 const AUTO_BACKUP_DISABLED = 0; 61 const AUTO_BACKUP_ENABLED = 1; 62 const AUTO_BACKUP_MANUAL = 2; 63 64 /** Automated backup storage in course backup filearea */ 65 const STORAGE_COURSE = 0; 66 /** Automated backup storage in specified directory */ 67 const STORAGE_DIRECTORY = 1; 68 /** Automated backup storage in course backup filearea and specified directory */ 69 const STORAGE_COURSE_AND_DIRECTORY = 2; 70 71 /** 72 * Runs the automated backups if required 73 * 74 * @global moodle_database $DB 75 */ 76 public static function run_automated_backup($rundirective = self::RUN_ON_SCHEDULE) { 77 global $CFG, $DB; 78 79 $status = true; 80 $emailpending = false; 81 $now = time(); 82 $config = get_config('backup'); 83 84 mtrace("Checking automated backup status",'...'); 85 $state = backup_cron_automated_helper::get_automated_backup_state($rundirective); 86 if ($state === backup_cron_automated_helper::STATE_DISABLED) { 87 mtrace('INACTIVE'); 88 return $state; 89 } else if ($state === backup_cron_automated_helper::STATE_RUNNING) { 90 mtrace('RUNNING'); 91 if ($rundirective == self::RUN_IMMEDIATELY) { 92 mtrace('Automated backups are already running. If this script is being run by cron this constitues an error. You will need to increase the time between executions within cron.'); 93 } else { 94 mtrace("automated backup are already running. Execution delayed"); 95 } 96 return $state; 97 } else { 98 mtrace('OK'); 99 } 100 backup_cron_automated_helper::set_state_running(); 101 102 mtrace("Getting admin info"); 103 $admin = get_admin(); 104 if (!$admin) { 105 mtrace("Error: No admin account was found"); 106 $state = false; 107 } 108 109 if ($status) { 110 mtrace("Checking courses"); 111 mtrace("Skipping deleted courses", '...'); 112 mtrace(sprintf("%d courses", backup_cron_automated_helper::remove_deleted_courses_from_schedule())); 113 } 114 115 if ($status) { 116 117 mtrace('Running required automated backups...'); 118 cron_trace_time_and_memory(); 119 120 // This could take a while! 121 core_php_time_limit::raise(); 122 raise_memory_limit(MEMORY_EXTRA); 123 124 $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, $now); 125 $showtime = "undefined"; 126 if ($nextstarttime > 0) { 127 $showtime = date('r', $nextstarttime); 128 } 129 130 $rs = $DB->get_recordset('course'); 131 foreach ($rs as $course) { 132 $backupcourse = $DB->get_record('backup_courses', array('courseid' => $course->id)); 133 if (!$backupcourse) { 134 $backupcourse = new stdClass; 135 $backupcourse->courseid = $course->id; 136 $backupcourse->laststatus = self::BACKUP_STATUS_NOTYETRUN; 137 $DB->insert_record('backup_courses', $backupcourse); 138 $backupcourse = $DB->get_record('backup_courses', array('courseid' => $course->id)); 139 } 140 141 // The last backup is considered as successful when OK or SKIPPED. 142 $lastbackupwassuccessful = ($backupcourse->laststatus == self::BACKUP_STATUS_SKIPPED || 143 $backupcourse->laststatus == self::BACKUP_STATUS_OK) && ( 144 $backupcourse->laststarttime > 0 && $backupcourse->lastendtime > 0); 145 146 // Assume that we are not skipping anything. 147 $skipped = false; 148 $skippedmessage = ''; 149 150 // Check if we are going to be running the backup now. 151 $shouldrunnow = (($backupcourse->nextstarttime > 0 && $backupcourse->nextstarttime < $now) 152 || $rundirective == self::RUN_IMMEDIATELY); 153 154 // If config backup_auto_skip_hidden is set to true, skip courses that are not visible. 155 if ($shouldrunnow && $config->backup_auto_skip_hidden) { 156 $skipped = ($config->backup_auto_skip_hidden && !$course->visible); 157 $skippedmessage = 'Not visible'; 158 } 159 160 // If config backup_auto_skip_modif_days is set to true, skip courses 161 // that have not been modified since the number of days defined. 162 if ($shouldrunnow && !$skipped && $lastbackupwassuccessful && $config->backup_auto_skip_modif_days) { 163 $timenotmodifsincedays = $now - ($config->backup_auto_skip_modif_days * DAYSECS); 164 // Check log if there were any modifications to the course content. 165 $logexists = self::is_course_modified($course->id, $timenotmodifsincedays); 166 $skipped = ($course->timemodified <= $timenotmodifsincedays && !$logexists); 167 $skippedmessage = 'Not modified in the past '.$config->backup_auto_skip_modif_days.' days'; 168 } 169 170 // If config backup_auto_skip_modif_prev is set to true, skip courses 171 // that have not been modified since previous backup. 172 if ($shouldrunnow && !$skipped && $lastbackupwassuccessful && $config->backup_auto_skip_modif_prev) { 173 // Check log if there were any modifications to the course content. 174 $logexists = self::is_course_modified($course->id, $backupcourse->laststarttime); 175 $skipped = ($course->timemodified <= $backupcourse->laststarttime && !$logexists); 176 $skippedmessage = 'Not modified since previous backup'; 177 } 178 179 // Check if the course is not scheduled to run right now. 180 if (!$shouldrunnow) { 181 $backupcourse->nextstarttime = $nextstarttime; 182 $DB->update_record('backup_courses', $backupcourse); 183 mtrace('Skipping ' . $course->fullname . ' (Not scheduled for backup until ' . $showtime . ')'); 184 } else { 185 if ($skipped) { // Must have been skipped for a reason. 186 $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED; 187 $backupcourse->nextstarttime = $nextstarttime; 188 $DB->update_record('backup_courses', $backupcourse); 189 mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')'); 190 mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime); 191 } else { 192 // Backup every non-skipped courses. 193 mtrace('Backing up '.$course->fullname.'...'); 194 195 // We have to send an email because we have included at least one backup. 196 $emailpending = true; 197 198 // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error). 199 if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) { 200 // Set laststarttime. 201 $starttime = time(); 202 203 $backupcourse->laststarttime = time(); 204 $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED; 205 $DB->update_record('backup_courses', $backupcourse); 206 207 $backupcourse->laststatus = self::launch_automated_backup($course, $backupcourse->laststarttime, 208 $admin->id); 209 $backupcourse->lastendtime = time(); 210 $backupcourse->nextstarttime = $nextstarttime; 211 212 $DB->update_record('backup_courses', $backupcourse); 213 214 mtrace("complete - next execution: $showtime"); 215 } 216 } 217 218 // Remove excess backups. 219 $removedcount = self::remove_excess_backups($course, $now); 220 } 221 } 222 $rs->close(); 223 } 224 225 //Send email to admin if necessary 226 if ($emailpending) { 227 mtrace("Sending email to admin"); 228 $message = ""; 229 230 $count = backup_cron_automated_helper::get_backup_status_array(); 231 $haserrors = ($count[self::BACKUP_STATUS_ERROR] != 0 || $count[self::BACKUP_STATUS_UNFINISHED] != 0); 232 233 // Build the message text. 234 // Summary. 235 $message .= get_string('summary') . "\n"; 236 $message .= "==================================================\n"; 237 $message .= ' ' . get_string('courses') . '; ' . array_sum($count) . "\n"; 238 $message .= ' ' . get_string('ok') . '; ' . $count[self::BACKUP_STATUS_OK] . "\n"; 239 $message .= ' ' . get_string('skipped') . '; ' . $count[self::BACKUP_STATUS_SKIPPED] . "\n"; 240 $message .= ' ' . get_string('error') . '; ' . $count[self::BACKUP_STATUS_ERROR] . "\n"; 241 $message .= ' ' . get_string('unfinished') . '; ' . $count[self::BACKUP_STATUS_UNFINISHED] . "\n"; 242 $message .= ' ' . get_string('warning') . '; ' . $count[self::BACKUP_STATUS_WARNING] . "\n"; 243 $message .= ' ' . get_string('backupnotyetrun') . '; ' . $count[self::BACKUP_STATUS_NOTYETRUN]."\n\n"; 244 245 //Reference 246 if ($haserrors) { 247 $message .= " ".get_string('backupfailed')."\n\n"; 248 $dest_url = "$CFG->wwwroot/report/backups/index.php"; 249 $message .= " ".get_string('backuptakealook','',$dest_url)."\n\n"; 250 //Set message priority 251 $admin->priority = 1; 252 //Reset unfinished to error 253 $DB->set_field('backup_courses','laststatus','0', array('laststatus'=>'2')); 254 } else { 255 $message .= " ".get_string('backupfinished')."\n"; 256 } 257 258 //Build the message subject 259 $site = get_site(); 260 $prefix = format_string($site->shortname, true, array('context' => context_course::instance(SITEID))).": "; 261 if ($haserrors) { 262 $prefix .= "[".strtoupper(get_string('error'))."] "; 263 } 264 $subject = $prefix.get_string('automatedbackupstatus', 'backup'); 265 266 //Send the message 267 $eventdata = new stdClass(); 268 $eventdata->modulename = 'moodle'; 269 $eventdata->userfrom = $admin; 270 $eventdata->userto = $admin; 271 $eventdata->subject = $subject; 272 $eventdata->fullmessage = $message; 273 $eventdata->fullmessageformat = FORMAT_PLAIN; 274 $eventdata->fullmessagehtml = ''; 275 $eventdata->smallmessage = ''; 276 277 $eventdata->component = 'moodle'; 278 $eventdata->name = 'backup'; 279 280 message_send($eventdata); 281 } 282 283 //Everything is finished stop backup_auto_running 284 backup_cron_automated_helper::set_state_running(false); 285 286 mtrace('Automated backups complete.'); 287 288 return $status; 289 } 290 291 /** 292 * Gets the results from the last automated backup that was run based upon 293 * the statuses of the courses that were looked at. 294 * 295 * @global moodle_database $DB 296 * @return array 297 */ 298 public static function get_backup_status_array() { 299 global $DB; 300 301 $result = array( 302 self::BACKUP_STATUS_ERROR => 0, 303 self::BACKUP_STATUS_OK => 0, 304 self::BACKUP_STATUS_UNFINISHED => 0, 305 self::BACKUP_STATUS_SKIPPED => 0, 306 self::BACKUP_STATUS_WARNING => 0, 307 self::BACKUP_STATUS_NOTYETRUN => 0 308 ); 309 310 $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) AS statuscount FROM {backup_courses} bc GROUP BY bc.laststatus'); 311 312 foreach ($statuses as $status) { 313 if (empty($status->statuscount)) { 314 $status->statuscount = 0; 315 } 316 $result[(int)$status->laststatus] += $status->statuscount; 317 } 318 319 return $result; 320 } 321 322 /** 323 * Works out the next time the automated backup should be run. 324 * 325 * @param mixed $ignoredtimezone all settings are in server timezone! 326 * @param int $now timestamp, should not be in the past, most likely time() 327 * @return int timestamp of the next execution at server time 328 */ 329 public static function calculate_next_automated_backup($ignoredtimezone, $now) { 330 331 $config = get_config('backup'); 332 333 $backuptime = new DateTime('@' . $now); 334 $backuptime->setTimezone(core_date::get_server_timezone_object()); 335 $backuptime->setTime($config->backup_auto_hour, $config->backup_auto_minute); 336 337 while ($backuptime->getTimestamp() < $now) { 338 $backuptime->add(new DateInterval('P1D')); 339 } 340 341 // Get number of days from backup date to execute backups. 342 $automateddays = substr($config->backup_auto_weekdays, $backuptime->format('w')) . $config->backup_auto_weekdays; 343 $daysfromnow = strpos($automateddays, "1"); 344 345 // Error, there are no days to schedule the backup for. 346 if ($daysfromnow === false) { 347 return 0; 348 } 349 350 if ($daysfromnow > 0) { 351 $backuptime->add(new DateInterval('P' . $daysfromnow . 'D')); 352 } 353 354 return $backuptime->getTimestamp(); 355 } 356 357 /** 358 * Launches a automated backup routine for the given course 359 * 360 * @param stdClass $course 361 * @param int $starttime 362 * @param int $userid 363 * @return bool 364 */ 365 public static function launch_automated_backup($course, $starttime, $userid) { 366 367 $outcome = self::BACKUP_STATUS_OK; 368 $config = get_config('backup'); 369 $dir = $config->backup_auto_destination; 370 $storage = (int)$config->backup_auto_storage; 371 372 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, 373 backup::MODE_AUTOMATED, $userid); 374 375 try { 376 377 // Set the default filename. 378 $format = $bc->get_format(); 379 $type = $bc->get_type(); 380 $id = $bc->get_id(); 381 $users = $bc->get_plan()->get_setting('users')->get_value(); 382 $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value(); 383 $bc->get_plan()->get_setting('filename')->set_value(backup_plan_dbops::get_default_backup_filename($format, $type, 384 $id, $users, $anonymised)); 385 386 $bc->set_status(backup::STATUS_AWAITING); 387 388 $bc->execute_plan(); 389 $results = $bc->get_results(); 390 $outcome = self::outcome_from_results($results); 391 $file = $results['backup_destination']; // May be empty if file already moved to target location. 392 393 // If we need to copy the backup file to an external dir and it is not writable, change status to error. 394 // This is a feature to prevent moodledata to be filled up and break a site when the admin misconfigured 395 // the automated backups storage type and destination directory. 396 if ($storage !== 0 && (empty($dir) || !file_exists($dir) || !is_dir($dir) || !is_writable($dir))) { 397 $bc->log('Specified backup directory is not writable - ', backup::LOG_ERROR, $dir); 398 $dir = null; 399 $outcome = self::BACKUP_STATUS_ERROR; 400 } 401 402 // Copy file only if there was no error. 403 if ($file && !empty($dir) && $storage !== 0 && $outcome != self::BACKUP_STATUS_ERROR) { 404 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, 405 !$config->backup_shortname); 406 if (!$file->copy_content_to($dir.'/'.$filename)) { 407 $bc->log('Attempt to copy backup file to the specified directory failed - ', 408 backup::LOG_ERROR, $dir); 409 $outcome = self::BACKUP_STATUS_ERROR; 410 } 411 if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) { 412 if (!$file->delete()) { 413 $outcome = self::BACKUP_STATUS_WARNING; 414 $bc->log('Attempt to delete the backup file from course automated backup area failed - ', 415 backup::LOG_WARNING, $file->get_filename()); 416 } 417 } 418 } 419 420 } catch (moodle_exception $e) { 421 $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header. 422 $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem. 423 $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information. 424 $outcome = self::BACKUP_STATUS_ERROR; 425 } 426 427 // Delete the backup file immediately if something went wrong. 428 if ($outcome === self::BACKUP_STATUS_ERROR) { 429 430 // Delete the file from file area if exists. 431 if (!empty($file)) { 432 $file->delete(); 433 } 434 435 // Delete file from external storage if exists. 436 if ($storage !== 0 && !empty($filename) && file_exists($dir.'/'.$filename)) { 437 @unlink($dir.'/'.$filename); 438 } 439 } 440 441 $bc->destroy(); 442 unset($bc); 443 444 return $outcome; 445 } 446 447 /** 448 * Returns the backup outcome by analysing its results. 449 * 450 * @param array $results returned by a backup 451 * @return int {@link self::BACKUP_STATUS_OK} and other constants 452 */ 453 public static function outcome_from_results($results) { 454 $outcome = self::BACKUP_STATUS_OK; 455 foreach ($results as $code => $value) { 456 // Each possible error and warning code has to be specified in this switch 457 // which basically analyses the results to return the correct backup status. 458 switch ($code) { 459 case 'missing_files_in_pool': 460 $outcome = self::BACKUP_STATUS_WARNING; 461 break; 462 } 463 // If we found the highest error level, we exit the loop. 464 if ($outcome == self::BACKUP_STATUS_ERROR) { 465 break; 466 } 467 } 468 return $outcome; 469 } 470 471 /** 472 * Removes deleted courses fromn the backup_courses table so that we don't 473 * waste time backing them up. 474 * 475 * @global moodle_database $DB 476 * @return int 477 */ 478 public static function remove_deleted_courses_from_schedule() { 479 global $DB; 480 $skipped = 0; 481 $sql = "SELECT bc.courseid FROM {backup_courses} bc WHERE bc.courseid NOT IN (SELECT c.id FROM {course} c)"; 482 $rs = $DB->get_recordset_sql($sql); 483 foreach ($rs as $deletedcourse) { 484 //Doesn't exist, so delete from backup tables 485 $DB->delete_records('backup_courses', array('courseid'=>$deletedcourse->courseid)); 486 $skipped++; 487 } 488 $rs->close(); 489 return $skipped; 490 } 491 492 /** 493 * Gets the state of the automated backup system. 494 * 495 * @global moodle_database $DB 496 * @return int One of self::STATE_* 497 */ 498 public static function get_automated_backup_state($rundirective = self::RUN_ON_SCHEDULE) { 499 global $DB; 500 501 $config = get_config('backup'); 502 $active = (int)$config->backup_auto_active; 503 $weekdays = (string)$config->backup_auto_weekdays; 504 505 // In case of automated backup also check that it is scheduled for at least one weekday. 506 if ($active === self::AUTO_BACKUP_DISABLED || 507 ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL) || 508 ($rundirective == self::RUN_ON_SCHEDULE && strpos($weekdays, '1') === false)) { 509 return self::STATE_DISABLED; 510 } else if (!empty($config->backup_auto_running)) { 511 // Detect if the backup_auto_running semaphore is a valid one 512 // by looking for recent activity in the backup_controllers table 513 // for backups of type backup::MODE_AUTOMATED 514 $timetosee = 60 * 90; // Time to consider in order to clean the semaphore 515 $params = array( 'purpose' => backup::MODE_AUTOMATED, 'timetolook' => (time() - $timetosee)); 516 if ($DB->record_exists_select('backup_controllers', 517 "operation = 'backup' AND type = 'course' AND purpose = :purpose AND timemodified > :timetolook", $params)) { 518 return self::STATE_RUNNING; // Recent activity found, still running 519 } else { 520 // No recent activity found, let's clean the semaphore 521 mtrace('Automated backups activity not found in last ' . (int)$timetosee/60 . ' minutes. Cleaning running status'); 522 backup_cron_automated_helper::set_state_running(false); 523 } 524 } 525 return self::STATE_OK; 526 } 527 528 /** 529 * Sets the state of the automated backup system. 530 * 531 * @param bool $running 532 * @return bool 533 */ 534 public static function set_state_running($running = true) { 535 if ($running === true) { 536 if (self::get_automated_backup_state() === self::STATE_RUNNING) { 537 throw new backup_helper_exception('backup_automated_already_running'); 538 } 539 set_config('backup_auto_running', '1', 'backup'); 540 } else { 541 unset_config('backup_auto_running', 'backup'); 542 } 543 return true; 544 } 545 546 /** 547 * Removes excess backups from a specified course. 548 * 549 * @param stdClass $course Course object 550 * @param int $now Starting time of the process 551 * @return bool Whether or not backups is being removed 552 */ 553 public static function remove_excess_backups($course, $now = null) { 554 $config = get_config('backup'); 555 $maxkept = (int)$config->backup_auto_max_kept; 556 $storage = $config->backup_auto_storage; 557 $deletedays = (int)$config->backup_auto_delete_days; 558 559 if ($maxkept == 0 && $deletedays == 0) { 560 // Means keep all backup files and never delete backup after x days. 561 return true; 562 } 563 564 if (!isset($now)) { 565 $now = time(); 566 } 567 568 // Clean up excess backups in the course backup filearea. 569 $deletedcoursebackups = false; 570 if ($storage == self::STORAGE_COURSE || $storage == self::STORAGE_COURSE_AND_DIRECTORY) { 571 $deletedcoursebackups = self::remove_excess_backups_from_course($course, $now); 572 } 573 574 // Clean up excess backups in the specified external directory. 575 $deleteddirectorybackups = false; 576 if ($storage == self::STORAGE_DIRECTORY || $storage == self::STORAGE_COURSE_AND_DIRECTORY) { 577 $deleteddirectorybackups = self::remove_excess_backups_from_directory($course, $now); 578 } 579 580 if ($deletedcoursebackups || $deleteddirectorybackups) { 581 return true; 582 } else { 583 return false; 584 } 585 } 586 587 /** 588 * Removes excess backups in the course backup filearea from a specified course. 589 * 590 * @param stdClass $course Course object 591 * @param int $now Starting time of the process 592 * @return bool Whether or not backups are being removed 593 */ 594 protected static function remove_excess_backups_from_course($course, $now) { 595 $fs = get_file_storage(); 596 $context = context_course::instance($course->id); 597 $component = 'backup'; 598 $filearea = 'automated'; 599 $itemid = 0; 600 $backupfiles = array(); 601 $backupfilesarea = $fs->get_area_files($context->id, $component, $filearea, $itemid, 'timemodified DESC', false); 602 // Store all the matching files into timemodified => stored_file array. 603 foreach ($backupfilesarea as $backupfile) { 604 $backupfiles[$backupfile->get_timemodified()] = $backupfile; 605 } 606 607 $backupstodelete = self::get_backups_to_delete($backupfiles, $now); 608 if ($backupstodelete) { 609 foreach ($backupstodelete as $backuptodelete) { 610 $backuptodelete->delete(); 611 } 612 mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from the automated filearea'); 613 return true; 614 } else { 615 return false; 616 } 617 } 618 619 /** 620 * Removes excess backups in the specified external directory from a specified course. 621 * 622 * @param stdClass $course Course object 623 * @param int $now Starting time of the process 624 * @return bool Whether or not backups are being removed 625 */ 626 protected static function remove_excess_backups_from_directory($course, $now) { 627 $config = get_config('backup'); 628 $dir = $config->backup_auto_destination; 629 630 $isnotvaliddir = !file_exists($dir) || !is_dir($dir) || !is_writable($dir); 631 if ($isnotvaliddir) { 632 mtrace('Error: ' . $dir . ' does not appear to be a valid directory'); 633 return false; 634 } 635 636 // Calculate backup filename regex, ignoring the date/time/info parts that can be 637 // variable, depending of languages, formats and automated backup settings. 638 $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-'; 639 $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#'; 640 641 // Store all the matching files into filename => timemodified array. 642 $backupfiles = array(); 643 foreach (scandir($dir) as $backupfile) { 644 // Skip files not matching the naming convention. 645 if (!preg_match($regex, $backupfile)) { 646 continue; 647 } 648 649 // Read the information contained in the backup itself. 650 try { 651 $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $backupfile); 652 } catch (backup_helper_exception $e) { 653 mtrace('Error: ' . $backupfile . ' does not appear to be a valid backup (' . $e->errorcode . ')'); 654 continue; 655 } 656 657 // Make sure this backup concerns the course and site we are looking for. 658 if ($bcinfo->format === backup::FORMAT_MOODLE && 659 $bcinfo->type === backup::TYPE_1COURSE && 660 $bcinfo->original_course_id == $course->id && 661 backup_general_helper::backup_is_samesite($bcinfo)) { 662 $backupfiles[$bcinfo->backup_date] = $backupfile; 663 } 664 } 665 666 $backupstodelete = self::get_backups_to_delete($backupfiles, $now); 667 if ($backupstodelete) { 668 foreach ($backupstodelete as $backuptodelete) { 669 unlink($dir . '/' . $backuptodelete); 670 } 671 mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from external directory'); 672 return true; 673 } else { 674 return false; 675 } 676 } 677 678 /** 679 * Get the list of backup files to delete depending on the automated backup settings. 680 * 681 * @param array $backupfiles Existing backup files 682 * @param int $now Starting time of the process 683 * @return array Backup files to delete 684 */ 685 protected static function get_backups_to_delete($backupfiles, $now) { 686 $config = get_config('backup'); 687 $maxkept = (int)$config->backup_auto_max_kept; 688 $deletedays = (int)$config->backup_auto_delete_days; 689 $minkept = (int)$config->backup_auto_min_kept; 690 691 // Sort by keys descending (newer to older filemodified). 692 krsort($backupfiles); 693 $tokeep = $maxkept; 694 if ($deletedays > 0) { 695 $deletedayssecs = $deletedays * DAYSECS; 696 $tokeep = 0; 697 $backupfileskeys = array_keys($backupfiles); 698 foreach ($backupfileskeys as $timemodified) { 699 $mustdeletebackup = $timemodified < ($now - $deletedayssecs); 700 if ($mustdeletebackup || $tokeep >= $maxkept) { 701 break; 702 } 703 $tokeep++; 704 } 705 706 if ($tokeep < $minkept) { 707 $tokeep = $minkept; 708 } 709 } 710 711 if (count($backupfiles) <= $tokeep) { 712 // There are less or equal matching files than the desired number to keep, there is nothing to clean up. 713 return false; 714 } else { 715 $backupstodelete = array_splice($backupfiles, $tokeep); 716 return $backupstodelete; 717 } 718 } 719 720 /** 721 * Check logs to find out if a course was modified since the given time. 722 * 723 * @param int $courseid course id to check 724 * @param int $since timestamp, from which to check 725 * 726 * @return bool true if the course was modified, false otherwise. This also returns false if no readers are enabled. This is 727 * intentional, since we cannot reliably determine if any modification was made or not. 728 */ 729 protected static function is_course_modified($courseid, $since) { 730 $logmang = get_log_manager(); 731 $readers = $logmang->get_readers('core\log\sql_reader'); 732 $where = "courseid = :courseid and timecreated > :since and crud <> 'r'"; 733 $params = array('courseid' => $courseid, 'since' => $since); 734 foreach ($readers as $reader) { 735 if ($reader->get_events_select_count($where, $params)) { 736 return true; 737 } 738 } 739 return false; 740 } 741 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |