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