Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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('ok') . ': ' . $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->fullname . ' (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->fullname . ' 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 mtrace("complete - next execution: $showtime"); 309 } 310 } 311 } 312 } 313 314 return $emailpending; 315 } 316 317 /** 318 * Check if we can skip this course backup. 319 * 320 * @param stdClass $backupcourse 321 * @param stdClass $course 322 * @param int $nextstarttime 323 * @return boolean 324 */ 325 private static function should_skip_course_backup($backupcourse, $course, $nextstarttime) { 326 global $DB; 327 328 $config = get_config('backup'); 329 $now = time(); 330 // Assume that we are not skipping anything. 331 $skipped = false; 332 $skippedmessage = ''; 333 334 // The last backup is considered as successful when OK or SKIPPED. 335 $lastbackupwassuccessful = ($backupcourse->laststatus == self::BACKUP_STATUS_SKIPPED || 336 $backupcourse->laststatus == self::BACKUP_STATUS_OK) && ( 337 $backupcourse->laststarttime > 0 && $backupcourse->lastendtime > 0); 338 339 // If config backup_auto_skip_hidden is set to true, skip courses that are not visible. 340 if ($config->backup_auto_skip_hidden) { 341 $skipped = ($config->backup_auto_skip_hidden && !$course->visible); 342 $skippedmessage = 'Not visible'; 343 } 344 345 // If config backup_auto_skip_modif_days is set to true, skip courses 346 // that have not been modified since the number of days defined. 347 if (!$skipped && $lastbackupwassuccessful && $config->backup_auto_skip_modif_days) { 348 $timenotmodifsincedays = $now - ($config->backup_auto_skip_modif_days * DAYSECS); 349 // Check log if there were any modifications to the course content. 350 $logexists = self::is_course_modified($course->id, $timenotmodifsincedays); 351 $skipped = ($course->timemodified <= $timenotmodifsincedays && !$logexists); 352 $skippedmessage = 'Not modified in the past '.$config->backup_auto_skip_modif_days.' days'; 353 } 354 355 // If config backup_auto_skip_modif_prev is set to true, skip courses 356 // that have not been modified since previous backup. 357 if (!$skipped && $lastbackupwassuccessful && $config->backup_auto_skip_modif_prev) { 358 // Check log if there were any modifications to the course content. 359 $logexists = self::is_course_modified($course->id, $backupcourse->laststarttime); 360 $skipped = ($course->timemodified <= $backupcourse->laststarttime && !$logexists); 361 $skippedmessage = 'Not modified since previous backup'; 362 } 363 364 if ($skipped) { // Must have been skipped for a reason. 365 $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED; 366 $backupcourse->nextstarttime = $nextstarttime; 367 $DB->update_record('backup_courses', $backupcourse); 368 mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')'); 369 mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . date('r', $nextstarttime)); 370 } 371 372 return $skipped; 373 } 374 375 /** 376 * Create course backup adhoc task 377 * 378 * @param stdClass $backupcourse 379 * @param stdClass $admin 380 * @return void 381 */ 382 private static function push_course_backup_adhoc_task($backupcourse, $admin) { 383 global $DB; 384 385 $asynctask = new \core\task\course_backup_task(); 386 $asynctask->set_blocking(false); 387 $asynctask->set_custom_data(array( 388 'courseid' => $backupcourse->courseid, 389 'adminid' => $admin->id 390 )); 391 \core\task\manager::queue_adhoc_task($asynctask); 392 393 $backupcourse->laststatus = self::BACKUP_STATUS_QUEUED; 394 $DB->update_record('backup_courses', $backupcourse); 395 } 396 397 /** 398 * Works out the next time the automated backup should be run. 399 * 400 * @param mixed $ignoredtimezone all settings are in server timezone! 401 * @param int $now timestamp, should not be in the past, most likely time() 402 * @return int timestamp of the next execution at server time 403 */ 404 public static function calculate_next_automated_backup($ignoredtimezone, $now) { 405 406 $config = get_config('backup'); 407 408 $backuptime = new DateTime('@' . $now); 409 $backuptime->setTimezone(core_date::get_server_timezone_object()); 410 $backuptime->setTime($config->backup_auto_hour, $config->backup_auto_minute); 411 412 while ($backuptime->getTimestamp() < $now) { 413 $backuptime->add(new DateInterval('P1D')); 414 } 415 416 // Get number of days from backup date to execute backups. 417 $automateddays = substr($config->backup_auto_weekdays, $backuptime->format('w')) . $config->backup_auto_weekdays; 418 $daysfromnow = strpos($automateddays, "1"); 419 420 // Error, there are no days to schedule the backup for. 421 if ($daysfromnow === false) { 422 return 0; 423 } 424 425 if ($daysfromnow > 0) { 426 $backuptime->add(new DateInterval('P' . $daysfromnow . 'D')); 427 } 428 429 return $backuptime->getTimestamp(); 430 } 431 432 /** 433 * Launches a automated backup routine for the given course 434 * 435 * @param stdClass $course 436 * @param int $starttime 437 * @param int $userid 438 * @return bool 439 */ 440 public static function launch_automated_backup($course, $starttime, $userid) { 441 442 $outcome = self::BACKUP_STATUS_OK; 443 $config = get_config('backup'); 444 $dir = $config->backup_auto_destination; 445 $storage = (int)$config->backup_auto_storage; 446 447 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, 448 backup::MODE_AUTOMATED, $userid); 449 450 try { 451 452 // Set the default filename. 453 $format = $bc->get_format(); 454 $type = $bc->get_type(); 455 $id = $bc->get_id(); 456 $users = $bc->get_plan()->get_setting('users')->get_value(); 457 $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value(); 458 $incfiles = (bool)$config->backup_auto_files; 459 $bc->get_plan()->get_setting('filename')->set_value(backup_plan_dbops::get_default_backup_filename($format, $type, 460 $id, $users, $anonymised, false, $incfiles)); 461 462 $bc->set_status(backup::STATUS_AWAITING); 463 464 $bc->execute_plan(); 465 $results = $bc->get_results(); 466 $outcome = self::outcome_from_results($results); 467 $file = $results['backup_destination']; // May be empty if file already moved to target location. 468 469 // If we need to copy the backup file to an external dir and it is not writable, change status to error. 470 // This is a feature to prevent moodledata to be filled up and break a site when the admin misconfigured 471 // the automated backups storage type and destination directory. 472 if ($storage !== 0 && (empty($dir) || !file_exists($dir) || !is_dir($dir) || !is_writable($dir))) { 473 $bc->log('Specified backup directory is not writable - ', backup::LOG_ERROR, $dir); 474 $dir = null; 475 $outcome = self::BACKUP_STATUS_ERROR; 476 } 477 478 // Copy file only if there was no error. 479 if ($file && !empty($dir) && $storage !== 0 && $outcome != self::BACKUP_STATUS_ERROR) { 480 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, 481 !$config->backup_shortname); 482 if (!$file->copy_content_to($dir.'/'.$filename)) { 483 $bc->log('Attempt to copy backup file to the specified directory failed - ', 484 backup::LOG_ERROR, $dir); 485 $outcome = self::BACKUP_STATUS_ERROR; 486 } 487 if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) { 488 if (!$file->delete()) { 489 $outcome = self::BACKUP_STATUS_WARNING; 490 $bc->log('Attempt to delete the backup file from course automated backup area failed - ', 491 backup::LOG_WARNING, $file->get_filename()); 492 } 493 } 494 } 495 496 } catch (moodle_exception $e) { 497 $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header. 498 $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem. 499 $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information. 500 $outcome = self::BACKUP_STATUS_ERROR; 501 } 502 503 // Delete the backup file immediately if something went wrong. 504 if ($outcome === self::BACKUP_STATUS_ERROR) { 505 506 // Delete the file from file area if exists. 507 if (!empty($file)) { 508 $file->delete(); 509 } 510 511 // Delete file from external storage if exists. 512 if ($storage !== 0 && !empty($filename) && file_exists($dir.'/'.$filename)) { 513 @unlink($dir.'/'.$filename); 514 } 515 } 516 517 $bc->destroy(); 518 unset($bc); 519 520 return $outcome; 521 } 522 523 /** 524 * Returns the backup outcome by analysing its results. 525 * 526 * @param array $results returned by a backup 527 * @return int {@link self::BACKUP_STATUS_OK} and other constants 528 */ 529 public static function outcome_from_results($results) { 530 $outcome = self::BACKUP_STATUS_OK; 531 foreach ($results as $code => $value) { 532 // Each possible error and warning code has to be specified in this switch 533 // which basically analyses the results to return the correct backup status. 534 switch ($code) { 535 case 'missing_files_in_pool': 536 $outcome = self::BACKUP_STATUS_WARNING; 537 break; 538 } 539 // If we found the highest error level, we exit the loop. 540 if ($outcome == self::BACKUP_STATUS_ERROR) { 541 break; 542 } 543 } 544 return $outcome; 545 } 546 547 /** 548 * Removes deleted courses fromn the backup_courses table so that we don't 549 * waste time backing them up. 550 * 551 * @return int 552 */ 553 public static function remove_deleted_courses_from_schedule() { 554 global $DB; 555 $skipped = 0; 556 $sql = "SELECT bc.courseid FROM {backup_courses} bc WHERE bc.courseid NOT IN (SELECT c.id FROM {course} c)"; 557 $rs = $DB->get_recordset_sql($sql); 558 foreach ($rs as $deletedcourse) { 559 // Doesn't exist, so delete from backup tables. 560 $DB->delete_records('backup_courses', array('courseid' => $deletedcourse->courseid)); 561 $skipped++; 562 } 563 $rs->close(); 564 return $skipped; 565 } 566 567 /** 568 * Try to get lock for automated backup. 569 * @param int $rundirective 570 * 571 * @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false. 572 */ 573 public static function get_automated_backup_lock($rundirective = self::RUN_ON_SCHEDULE) { 574 $config = get_config('backup'); 575 $active = (int)$config->backup_auto_active; 576 $weekdays = (string)$config->backup_auto_weekdays; 577 578 mtrace("Checking automated backup status", '...'); 579 $locktype = 'automated_backup'; 580 $resource = 'queue_backup_jobs_running'; 581 $lockfactory = \core\lock\lock_config::get_lock_factory($locktype); 582 583 // In case of automated backup also check that it is scheduled for at least one weekday. 584 if ($active === self::AUTO_BACKUP_DISABLED || 585 ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL) || 586 ($rundirective == self::RUN_ON_SCHEDULE && strpos($weekdays, '1') === false)) { 587 mtrace('INACTIVE'); 588 return false; 589 } 590 591 if (!$lock = $lockfactory->get_lock($resource, 10)) { 592 return false; 593 } 594 595 mtrace('OK'); 596 return $lock; 597 } 598 599 /** 600 * Removes excess backups from a specified course. 601 * 602 * @param stdClass $course Course object 603 * @param int $now Starting time of the process 604 * @return bool Whether or not backups is being removed 605 */ 606 public static function remove_excess_backups($course, $now = null) { 607 $config = get_config('backup'); 608 $maxkept = (int)$config->backup_auto_max_kept; 609 $storage = $config->backup_auto_storage; 610 $deletedays = (int)$config->backup_auto_delete_days; 611 612 if ($maxkept == 0 && $deletedays == 0) { 613 // Means keep all backup files and never delete backup after x days. 614 return true; 615 } 616 617 if (!isset($now)) { 618 $now = time(); 619 } 620 621 // Clean up excess backups in the course backup filearea. 622 $deletedcoursebackups = false; 623 if ($storage == self::STORAGE_COURSE || $storage == self::STORAGE_COURSE_AND_DIRECTORY) { 624 $deletedcoursebackups = self::remove_excess_backups_from_course($course, $now); 625 } 626 627 // Clean up excess backups in the specified external directory. 628 $deleteddirectorybackups = false; 629 if ($storage == self::STORAGE_DIRECTORY || $storage == self::STORAGE_COURSE_AND_DIRECTORY) { 630 $deleteddirectorybackups = self::remove_excess_backups_from_directory($course, $now); 631 } 632 633 if ($deletedcoursebackups || $deleteddirectorybackups) { 634 return true; 635 } else { 636 return false; 637 } 638 } 639 640 /** 641 * Removes excess backups in the course backup filearea from a specified course. 642 * 643 * @param stdClass $course Course object 644 * @param int $now Starting time of the process 645 * @return bool Whether or not backups are being removed 646 */ 647 protected static function remove_excess_backups_from_course($course, $now) { 648 $fs = get_file_storage(); 649 $context = context_course::instance($course->id); 650 $component = 'backup'; 651 $filearea = 'automated'; 652 $itemid = 0; 653 $backupfiles = array(); 654 $backupfilesarea = $fs->get_area_files($context->id, $component, $filearea, $itemid, 'timemodified DESC', false); 655 // Store all the matching files into timemodified => stored_file array. 656 foreach ($backupfilesarea as $backupfile) { 657 $backupfiles[$backupfile->get_timemodified()] = $backupfile; 658 } 659 660 $backupstodelete = self::get_backups_to_delete($backupfiles, $now); 661 if ($backupstodelete) { 662 foreach ($backupstodelete as $backuptodelete) { 663 $backuptodelete->delete(); 664 } 665 mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from the automated filearea'); 666 return true; 667 } else { 668 return false; 669 } 670 } 671 672 /** 673 * Removes excess backups in the specified external directory from a specified course. 674 * 675 * @param stdClass $course Course object 676 * @param int $now Starting time of the process 677 * @return bool Whether or not backups are being removed 678 */ 679 protected static function remove_excess_backups_from_directory($course, $now) { 680 $config = get_config('backup'); 681 $dir = $config->backup_auto_destination; 682 683 $isnotvaliddir = !file_exists($dir) || !is_dir($dir) || !is_writable($dir); 684 if ($isnotvaliddir) { 685 mtrace('Error: ' . $dir . ' does not appear to be a valid directory'); 686 return false; 687 } 688 689 // Calculate backup filename regex, ignoring the date/time/info parts that can be 690 // variable, depending of languages, formats and automated backup settings. 691 $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-'; 692 $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#'; 693 694 // Store all the matching files into filename => timemodified array. 695 $backupfiles = array(); 696 foreach (scandir($dir) as $backupfile) { 697 // Skip files not matching the naming convention. 698 if (!preg_match($regex, $backupfile)) { 699 continue; 700 } 701 702 // Read the information contained in the backup itself. 703 try { 704 $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $backupfile); 705 } catch (backup_helper_exception $e) { 706 mtrace('Error: ' . $backupfile . ' does not appear to be a valid backup (' . $e->errorcode . ')'); 707 continue; 708 } 709 710 // Make sure this backup concerns the course and site we are looking for. 711 if ($bcinfo->format === backup::FORMAT_MOODLE && 712 $bcinfo->type === backup::TYPE_1COURSE && 713 $bcinfo->original_course_id == $course->id && 714 backup_general_helper::backup_is_samesite($bcinfo)) { 715 $backupfiles[$bcinfo->backup_date] = $backupfile; 716 } 717 } 718 719 $backupstodelete = self::get_backups_to_delete($backupfiles, $now); 720 if ($backupstodelete) { 721 foreach ($backupstodelete as $backuptodelete) { 722 unlink($dir . '/' . $backuptodelete); 723 } 724 mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from external directory'); 725 return true; 726 } else { 727 return false; 728 } 729 } 730 731 /** 732 * Get the list of backup files to delete depending on the automated backup settings. 733 * 734 * @param array $backupfiles Existing backup files 735 * @param int $now Starting time of the process 736 * @return array Backup files to delete 737 */ 738 protected static function get_backups_to_delete($backupfiles, $now) { 739 $config = get_config('backup'); 740 $maxkept = (int)$config->backup_auto_max_kept; 741 $deletedays = (int)$config->backup_auto_delete_days; 742 $minkept = (int)$config->backup_auto_min_kept; 743 744 // Sort by keys descending (newer to older filemodified). 745 krsort($backupfiles); 746 $tokeep = $maxkept; 747 if ($deletedays > 0) { 748 $deletedayssecs = $deletedays * DAYSECS; 749 $tokeep = 0; 750 $backupfileskeys = array_keys($backupfiles); 751 foreach ($backupfileskeys as $timemodified) { 752 $mustdeletebackup = $timemodified < ($now - $deletedayssecs); 753 if ($mustdeletebackup || $tokeep >= $maxkept) { 754 break; 755 } 756 $tokeep++; 757 } 758 759 if ($tokeep < $minkept) { 760 $tokeep = $minkept; 761 } 762 } 763 764 if (count($backupfiles) <= $tokeep) { 765 // There are less or equal matching files than the desired number to keep, there is nothing to clean up. 766 return false; 767 } else { 768 $backupstodelete = array_splice($backupfiles, $tokeep); 769 return $backupstodelete; 770 } 771 } 772 773 /** 774 * Check logs to find out if a course was modified since the given time. 775 * 776 * @param int $courseid course id to check 777 * @param int $since timestamp, from which to check 778 * 779 * @return bool true if the course was modified, false otherwise. This also returns false if no readers are enabled. This is 780 * intentional, since we cannot reliably determine if any modification was made or not. 781 */ 782 protected static function is_course_modified($courseid, $since) { 783 $logmang = get_log_manager(); 784 $readers = $logmang->get_readers('core\log\sql_reader'); 785 $params = array('courseid' => $courseid, 'since' => $since); 786 787 foreach ($readers as $readerpluginname => $reader) { 788 $where = "courseid = :courseid and timecreated > :since and crud <> 'r'"; 789 790 // Prevent logs of prevous backups causing a false positive. 791 if ($readerpluginname != 'logstore_legacy') { 792 $where .= " and target <> 'course_backup'"; 793 } 794 795 if ($reader->get_events_select_count($where, $params)) { 796 return true; 797 } 798 } 799 return false; 800 } 801 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body