|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00018 00032 abstract class backup_cron_automated_helper { 00033 00035 const STATE_OK = 0; 00037 const STATE_DISABLED = 1; 00039 const STATE_RUNNING = 2; 00040 00042 const BACKUP_STATUS_OK = 1; 00044 const BACKUP_STATUS_ERROR = 0; 00046 const BACKUP_STATUS_UNFINISHED = 2; 00048 const BACKUP_STATUS_SKIPPED = 3; 00049 00051 const RUN_ON_SCHEDULE = 0; 00053 const RUN_IMMEDIATELY = 1; 00054 00055 const AUTO_BACKUP_DISABLED = 0; 00056 const AUTO_BACKUP_ENABLED = 1; 00057 const AUTO_BACKUP_MANUAL = 2; 00058 00064 public static function run_automated_backup($rundirective = self::RUN_ON_SCHEDULE) { 00065 global $CFG, $DB; 00066 00067 $status = true; 00068 $emailpending = false; 00069 $now = time(); 00070 00071 mtrace("Checking automated backup status",'...'); 00072 $state = backup_cron_automated_helper::get_automated_backup_state($rundirective); 00073 if ($state === backup_cron_automated_helper::STATE_DISABLED) { 00074 mtrace('INACTIVE'); 00075 return $state; 00076 } else if ($state === backup_cron_automated_helper::STATE_RUNNING) { 00077 mtrace('RUNNING'); 00078 if ($rundirective == self::RUN_IMMEDIATELY) { 00079 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.'); 00080 } else { 00081 mtrace("automated backup are already running. Execution delayed"); 00082 } 00083 return $state; 00084 } else { 00085 mtrace('OK'); 00086 } 00087 backup_cron_automated_helper::set_state_running(); 00088 00089 mtrace("Getting admin info"); 00090 $admin = get_admin(); 00091 if (!$admin) { 00092 mtrace("Error: No admin account was found"); 00093 $state = false; 00094 } 00095 00096 if ($status) { 00097 mtrace("Checking courses"); 00098 mtrace("Skipping deleted courses", '...'); 00099 mtrace(sprintf("%d courses", backup_cron_automated_helper::remove_deleted_courses_from_schedule())); 00100 } 00101 00102 if ($status) { 00103 00104 mtrace('Running required automated backups...'); 00105 00106 // This could take a while! 00107 @set_time_limit(0); 00108 raise_memory_limit(MEMORY_EXTRA); 00109 00110 $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup($admin->timezone, $now); 00111 $showtime = "undefined"; 00112 if ($nextstarttime > 0) { 00113 $showtime = userdate($nextstarttime,"",$admin->timezone); 00114 } 00115 00116 $rs = $DB->get_recordset('course'); 00117 foreach ($rs as $course) { 00118 $backupcourse = $DB->get_record('backup_courses', array('courseid'=>$course->id)); 00119 if (!$backupcourse) { 00120 $backupcourse = new stdClass; 00121 $backupcourse->courseid = $course->id; 00122 $DB->insert_record('backup_courses',$backupcourse); 00123 $backupcourse = $DB->get_record('backup_courses', array('courseid'=>$course->id)); 00124 } 00125 00126 // Skip courses that do not yet need backup 00127 $skipped = !(($backupcourse->nextstarttime >= 0 && $backupcourse->nextstarttime < $now) || $rundirective == self::RUN_IMMEDIATELY); 00128 // Skip backup of unavailable courses that have remained unmodified in a month 00129 if (!$skipped && empty($course->visible) && ($now - $course->timemodified) > 31*24*60*60) { //Hidden + settings were unmodified last month 00130 //Check log if there were any modifications to the course content 00131 $sqlwhere = "course=:courseid AND time>:time AND ". $DB->sql_like('action', ':action', false, true, true); 00132 $params = array('courseid' => $course->id, 'time' => $now-31*24*60*60, 'action' => '%view%'); 00133 $logexists = $DB->record_exists_select('log', $sqlwhere, $params); 00134 if (!$logexists) { 00135 $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED; 00136 $backupcourse->nextstarttime = $nextstarttime; 00137 $DB->update_record('backup_courses', $backupcourse); 00138 mtrace('Skipping unchanged course '.$course->fullname); 00139 $skipped = true; 00140 } 00141 } 00142 //Now we backup every non-skipped course 00143 if (!$skipped) { 00144 mtrace('Backing up '.$course->fullname, '...'); 00145 00146 //We have to send a email because we have included at least one backup 00147 $emailpending = true; 00148 00149 //Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error) 00150 if ($backupcourse->laststatus != 2) { 00151 //Set laststarttime 00152 $starttime = time(); 00153 00154 $backupcourse->laststarttime = time(); 00155 $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED; 00156 $DB->update_record('backup_courses', $backupcourse); 00157 00158 $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id); 00159 $backupcourse->lastendtime = time(); 00160 $backupcourse->nextstarttime = $nextstarttime; 00161 00162 $DB->update_record('backup_courses', $backupcourse); 00163 00164 if ($backupcourse->laststatus) { 00165 // Clean up any excess course backups now that we have 00166 // taken a successful backup. 00167 $removedcount = backup_cron_automated_helper::remove_excess_backups($course); 00168 } 00169 } 00170 00171 mtrace("complete - next execution: $showtime"); 00172 } 00173 } 00174 $rs->close(); 00175 } 00176 00177 //Send email to admin if necessary 00178 if ($emailpending) { 00179 mtrace("Sending email to admin"); 00180 $message = ""; 00181 00182 $count = backup_cron_automated_helper::get_backup_status_array(); 00183 $haserrors = ($count[backup_cron_automated_helper::BACKUP_STATUS_ERROR] != 0 || $count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED] != 0); 00184 00185 //Build the message text 00186 //Summary 00187 $message .= get_string('summary')."\n"; 00188 $message .= "==================================================\n"; 00189 $message .= " ".get_string('courses').": ".array_sum($count)."\n"; 00190 $message .= " ".get_string('ok').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_OK]."\n"; 00191 $message .= " ".get_string('skipped').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_SKIPPED]."\n"; 00192 $message .= " ".get_string('error').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_ERROR]."\n"; 00193 $message .= " ".get_string('unfinished').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED]."\n\n"; 00194 00195 //Reference 00196 if ($haserrors) { 00197 $message .= " ".get_string('backupfailed')."\n\n"; 00198 $dest_url = "$CFG->wwwroot/report/backups/index.php"; 00199 $message .= " ".get_string('backuptakealook','',$dest_url)."\n\n"; 00200 //Set message priority 00201 $admin->priority = 1; 00202 //Reset unfinished to error 00203 $DB->set_field('backup_courses','laststatus','0', array('laststatus'=>'2')); 00204 } else { 00205 $message .= " ".get_string('backupfinished')."\n"; 00206 } 00207 00208 //Build the message subject 00209 $site = get_site(); 00210 $prefix = format_string($site->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, SITEID))).": "; 00211 if ($haserrors) { 00212 $prefix .= "[".strtoupper(get_string('error'))."] "; 00213 } 00214 $subject = $prefix.get_string('automatedbackupstatus', 'backup'); 00215 00216 //Send the message 00217 $eventdata = new stdClass(); 00218 $eventdata->modulename = 'moodle'; 00219 $eventdata->userfrom = $admin; 00220 $eventdata->userto = $admin; 00221 $eventdata->subject = $subject; 00222 $eventdata->fullmessage = $message; 00223 $eventdata->fullmessageformat = FORMAT_PLAIN; 00224 $eventdata->fullmessagehtml = ''; 00225 $eventdata->smallmessage = ''; 00226 00227 $eventdata->component = 'moodle'; 00228 $eventdata->name = 'backup'; 00229 00230 message_send($eventdata); 00231 } 00232 00233 //Everything is finished stop backup_auto_running 00234 backup_cron_automated_helper::set_state_running(false); 00235 00236 mtrace('Automated backups complete.'); 00237 00238 return $status; 00239 } 00240 00248 public static function get_backup_status_array() { 00249 global $DB; 00250 00251 $result = array( 00252 self::BACKUP_STATUS_ERROR => 0, 00253 self::BACKUP_STATUS_OK => 0, 00254 self::BACKUP_STATUS_UNFINISHED => 0, 00255 self::BACKUP_STATUS_SKIPPED => 0, 00256 ); 00257 00258 $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) statuscount FROM {backup_courses} bc GROUP BY bc.laststatus'); 00259 00260 foreach ($statuses as $status) { 00261 if (empty($status->statuscount)) { 00262 $status->statuscount = 0; 00263 } 00264 $result[(int)$status->laststatus] += $status->statuscount; 00265 } 00266 00267 return $result; 00268 } 00269 00277 public static function calculate_next_automated_backup($timezone, $now) { 00278 00279 $result = -1; 00280 $config = get_config('backup'); 00281 $midnight = usergetmidnight($now, $timezone); 00282 $date = usergetdate($now, $timezone); 00283 00284 //Get number of days (from today) to execute backups 00285 $automateddays = substr($config->backup_auto_weekdays,$date['wday']) . $config->backup_auto_weekdays; 00286 $daysfromtoday = strpos($automateddays, "1"); 00287 if (empty($daysfromtoday)) { 00288 $daysfromtoday = 1; 00289 } 00290 00291 //If some day has been found 00292 if ($daysfromtoday !== false) { 00293 //Calculate distance 00294 $dist = ($daysfromtoday * 86400) + //Days distance 00295 ($config->backup_auto_hour * 3600) + //Hours distance 00296 ($config->backup_auto_minute * 60); //Minutes distance 00297 $result = $midnight + $dist; 00298 } 00299 00300 //If that time is past, call the function recursively to obtain the next valid day 00301 if ($result > 0 && $result < time()) { 00302 $result = self::calculate_next_automated_backup($timezone, $result); 00303 } 00304 00305 return $result; 00306 } 00307 00316 public static function launch_automated_backup($course, $starttime, $userid) { 00317 00318 $config = get_config('backup'); 00319 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid); 00320 00321 try { 00322 00323 $settings = array( 00324 'users' => 'backup_auto_users', 00325 'role_assignments' => 'backup_auto_role_assignments', 00326 'user_files' => 'backup_auto_user_files', 00327 'activities' => 'backup_auto_activities', 00328 'blocks' => 'backup_auto_blocks', 00329 'filters' => 'backup_auto_filters', 00330 'comments' => 'backup_auto_comments', 00331 'completion_information' => 'backup_auto_userscompletion', 00332 'logs' => 'backup_auto_logs', 00333 'histories' => 'backup_auto_histories' 00334 ); 00335 foreach ($settings as $setting => $configsetting) { 00336 if ($bc->get_plan()->setting_exists($setting)) { 00337 $bc->get_plan()->get_setting($setting)->set_value($config->{$configsetting}); 00338 } 00339 } 00340 00341 // Set the default filename 00342 $format = $bc->get_format(); 00343 $type = $bc->get_type(); 00344 $id = $bc->get_id(); 00345 $users = $bc->get_plan()->get_setting('users')->get_value(); 00346 $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value(); 00347 $bc->get_plan()->get_setting('filename')->set_value(backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised)); 00348 00349 $bc->set_status(backup::STATUS_AWAITING); 00350 00351 $outcome = $bc->execute_plan(); 00352 $results = $bc->get_results(); 00353 $file = $results['backup_destination']; 00354 $dir = $config->backup_auto_destination; 00355 $storage = (int)$config->backup_auto_storage; 00356 if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) { 00357 $dir = null; 00358 } 00359 if (!empty($dir) && $storage !== 0) { 00360 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, true); 00361 $outcome = $file->copy_content_to($dir.'/'.$filename); 00362 if ($outcome && $storage === 1) { 00363 $file->delete(); 00364 } 00365 } 00366 00367 $outcome = true; 00368 } catch (backup_exception $e) { 00369 $bc->log('backup_auto_failed_on_course', backup::LOG_WARNING, $course->shortname); 00370 $outcome = false; 00371 } 00372 00373 $bc->destroy(); 00374 unset($bc); 00375 00376 return true; 00377 } 00378 00386 public static function remove_deleted_courses_from_schedule() { 00387 global $DB; 00388 $skipped = 0; 00389 $sql = "SELECT bc.courseid FROM {backup_courses} bc WHERE bc.courseid NOT IN (SELECT c.id FROM {course} c)"; 00390 $rs = $DB->get_recordset_sql($sql); 00391 foreach ($rs as $deletedcourse) { 00392 //Doesn't exist, so delete from backup tables 00393 $DB->delete_records('backup_courses', array('courseid'=>$deletedcourse->courseid)); 00394 $skipped++; 00395 } 00396 $rs->close(); 00397 return $skipped; 00398 } 00399 00406 public static function get_automated_backup_state($rundirective = self::RUN_ON_SCHEDULE) { 00407 global $DB; 00408 00409 $config = get_config('backup'); 00410 $active = (int)$config->backup_auto_active; 00411 if ($active === self::AUTO_BACKUP_DISABLED || ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL)) { 00412 return self::STATE_DISABLED; 00413 } else if (!empty($config->backup_auto_running)) { 00414 // Detect if the backup_auto_running semaphore is a valid one 00415 // by looking for recent activity in the backup_controllers table 00416 // for backups of type backup::MODE_AUTOMATED 00417 $timetosee = 60 * 90; // Time to consider in order to clean the semaphore 00418 $params = array( 'purpose' => backup::MODE_AUTOMATED, 'timetolook' => (time() - $timetosee)); 00419 if ($DB->record_exists_select('backup_controllers', 00420 "operation = 'backup' AND type = 'course' AND purpose = :purpose AND timemodified > :timetolook", $params)) { 00421 return self::STATE_RUNNING; // Recent activity found, still running 00422 } else { 00423 // No recent activity found, let's clean the semaphore 00424 mtrace('Automated backups activity not found in last ' . (int)$timetosee/60 . ' minutes. Cleaning running status'); 00425 backup_cron_automated_helper::set_state_running(false); 00426 } 00427 } 00428 return self::STATE_OK; 00429 } 00430 00437 public static function set_state_running($running = true) { 00438 if ($running === true) { 00439 if (self::get_automated_backup_state() === self::STATE_RUNNING) { 00440 throw new backup_exception('backup_automated_already_running'); 00441 } 00442 set_config('backup_auto_running', '1', 'backup'); 00443 } else { 00444 unset_config('backup_auto_running', 'backup'); 00445 } 00446 return true; 00447 } 00448 00457 public static function remove_excess_backups($course) { 00458 $config = get_config('backup'); 00459 $keep = (int)$config->backup_auto_keep; 00460 $storage = $config->backup_auto_storage; 00461 $dir = $config->backup_auto_destination; 00462 00463 $backupword = str_replace(' ', '_', moodle_strtolower(get_string('backupfilename'))); 00464 $backupword = trim(clean_filename($backupword), '_'); 00465 00466 if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) { 00467 $dir = null; 00468 } 00469 00470 // Clean up excess backups in the course backup filearea 00471 if ($storage == 0 || $storage == 2) { 00472 $fs = get_file_storage(); 00473 $context = get_context_instance(CONTEXT_COURSE, $course->id); 00474 $component = 'backup'; 00475 $filearea = 'automated'; 00476 $itemid = 0; 00477 $files = array(); 00478 // Store all the matching files into timemodified => stored_file array 00479 foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) { 00480 if (strpos($file->get_filename(), $backupword) !== 0) { 00481 continue; 00482 } 00483 $files[$file->get_timemodified()] = $file; 00484 } 00485 if (count($files) <= $keep) { 00486 // There are less matching files than the desired number to keep 00487 // do there is nothing to clean up. 00488 return 0; 00489 } 00490 // Sort by keys descending (newer to older filemodified) 00491 krsort($files); 00492 $remove = array_splice($files, $keep); 00493 foreach ($remove as $file) { 00494 $file->delete(); 00495 } 00496 //mtrace('Removed '.count($remove).' old backup file(s) from the automated filearea'); 00497 } 00498 00499 // Clean up excess backups in the specified external directory 00500 if (!empty($dir) && ($storage == 1 || $storage == 2)) { 00501 // Calculate backup filename regex, ignoring the date/time/info parts that can be 00502 // variable, depending of languages, formats and automated backup settings 00503 $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$course->id . '-'; 00504 $regex = '#^'.preg_quote($filename, '#').'.*\.mbz$#'; 00505 00506 // Store all the matching files into fullpath => timemodified array 00507 $files = array(); 00508 foreach (scandir($dir) as $file) { 00509 if (preg_match($regex, $file, $matches)) { 00510 $files[$file] = filemtime($dir . '/' . $file); 00511 } 00512 } 00513 if (count($files) <= $keep) { 00514 // There are less matching files than the desired number to keep 00515 // do there is nothing to clean up. 00516 return 0; 00517 } 00518 // Sort by values descending (newer to older filemodified) 00519 arsort($files); 00520 $remove = array_splice($files, $keep); 00521 foreach (array_keys($remove) as $file) { 00522 unlink($dir . '/' . $file); 00523 } 00524 //mtrace('Removed '.count($remove).' old backup file(s) from external directory'); 00525 } 00526 00527 return true; 00528 } 00529 }