[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/backup/util/helper/ -> backup_cron_helper.class.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1