MDL-25863 backup - take rid of dupe code for filename calculations
[moodle.git] / backup / util / helper / backup_cron_helper.class.php
1 <?php
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/>.
19 /**
20  * Utility helper for automated backups run through cron.
21  *
22  * @package    core
23  * @subpackage backup
24  * @copyright  2010 Sam Hemelryk
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
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 {
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;
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;
50     /** Run if required by the schedule set in config. Default. **/
51     const RUN_ON_SCHEDULE = 0;
52     /** Run immediately. **/
53     const RUN_IMMEDIATELY = 1;
55     const AUTO_BACKUP_DISABLED = 0;
56     const AUTO_BACKUP_ENABLED = 1;
57     const AUTO_BACKUP_MANUAL = 2;
59     /**
60      * Runs the automated backups if required
61      *
62      * @global moodle_database $DB
63      */
64     public static function run_automated_backup($rundirective = self::RUN_ON_SCHEDULE) {
65         global $CFG, $DB;
67         $status = true;
68         $emailpending = false;
69         $now = time();
71         mtrace("Checking automated backup status",'...');
72         $state = backup_cron_automated_helper::get_automated_backup_state($rundirective);
73         if ($state === backup_cron_automated_helper::STATE_DISABLED) {
74             mtrace('INACTIVE');
75             return $state;
76         } else if ($state === backup_cron_automated_helper::STATE_RUNNING) {
77             mtrace('RUNNING');
78             if ($rundirective == self::RUN_IMMEDIATELY) {
79                 mtrace('automated backups are already. If this script is being run by cron this constitues an error. You will need to increase the time between executions within cron.');
80             } else {
81                 mtrace("automated backup are already running. Execution delayed");
82             }
83             return $state;
84         } else {
85             mtrace('OK');
86         }
87         backup_cron_automated_helper::set_state_running();
89         mtrace("Getting admin info");
90         $admin = get_admin();
91         if (!$admin) {
92             mtrace("Error: No admin account was found");
93             $state = false;
94         }
96         if ($status) {
97             mtrace("Checking courses");
98             mtrace("Skipping deleted courses", '...');
99             mtrace(sprintf("%d courses", backup_cron_automated_helper::remove_deleted_courses_from_schedule()));
100         }
102         if ($status) {
104             mtrace('Running required automated backups...');
106             // This could take a while!
107             @set_time_limit(0);
108             raise_memory_limit(MEMORY_EXTRA);
110             $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup($admin->timezone, $now);
111             $showtime = "undefined";
112             if ($nextstarttime > 0) {
113                 $showtime = userdate($nextstarttime,"",$admin->timezone);
114             }
116             $rs = $DB->get_recordset('course');
117             foreach ($rs as $course) {
118                 $backupcourse = $DB->get_record('backup_courses', array('courseid'=>$course->id));
119                 if (!$backupcourse) {
120                     $backupcourse = new stdClass;
121                     $backupcourse->courseid = $course->id;
122                     $DB->insert_record('backup_courses',$backupcourse);
123                     $backupcourse = $DB->get_record('backup_courses', array('courseid'=>$course->id));
124                 }
126                 // Skip backup of unavailable courses that have remained unmodified in a month
127                 $skipped = false;
128                 if (empty($course->visible) && ($now - $course->timemodified) > 31*24*60*60) {  //Hidden + unmodified last month
129                     $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
130                     $DB->update_record('backup_courses', $backupcourse);
131                     mtrace('Skipping unchanged course '.$course->fullname);
132                     $skipped = true;
133                 } else if (($backupcourse->nextstarttime >= 0 && $backupcourse->nextstarttime < $now) || $rundirective == self::RUN_IMMEDIATELY) {
134                     mtrace('Backing up '.$course->fullname, '...');
136                     //We have to send a email because we have included at least one backup
137                     $emailpending = true;
139                     //Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error)
140                     if ($backupcourse->laststatus != 2) {
141                         //Set laststarttime
142                         $starttime = time();
144                         $backupcourse->laststarttime = time();
145                         $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED;
146                         $DB->update_record('backup_courses', $backupcourse);
148                         $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id);
149                         $backupcourse->lastendtime = time();
150                         $backupcourse->nextstarttime = $nextstarttime;
152                         $DB->update_record('backup_courses', $backupcourse);
154                         if ($backupcourse->laststatus) {
155                             // Clean up any excess course backups now that we have
156                             // taken a successful backup.
157                             $removedcount = backup_cron_automated_helper::remove_excess_backups($course);
158                         }
159                     }
161                     mtrace("complete - next execution: $showtime");
162                 }
163             }
164             $rs->close();
165         }
167         //Send email to admin if necessary
168         if ($emailpending) {
169             mtrace("Sending email to admin");
170             $message = "";
172             $count = backup_cron_automated_helper::get_backup_status_array();
173             $haserrors = ($count[backup_cron_automated_helper::BACKUP_STATUS_ERROR] != 0 || $count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED] != 0);
175             //Build the message text
176             //Summary
177             $message .= get_string('summary')."\n";
178             $message .= "==================================================\n";
179             $message .= "  ".get_string('courses').": ".array_sum($count)."\n";
180             $message .= "  ".get_string('ok').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_OK]."\n";
181             $message .= "  ".get_string('skipped').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_SKIPPED]."\n";
182             $message .= "  ".get_string('error').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_ERROR]."\n";
183             $message .= "  ".get_string('unfinished').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED]."\n\n";
185             //Reference
186             if ($haserrors) {
187                 $message .= "  ".get_string('backupfailed')."\n\n";
188                 $dest_url = "$CFG->wwwroot/$CFG->admin/report/backups/index.php";
189                 $message .= "  ".get_string('backuptakealook','',$dest_url)."\n\n";
190                 //Set message priority
191                 $admin->priority = 1;
192                 //Reset unfinished to error
193                 $DB->set_field('backup_courses','laststatus','0', array('laststatus'=>'2'));
194             } else {
195                 $message .= "  ".get_string('backupfinished')."\n";
196             }
198             //Build the message subject
199             $site = get_site();
200             $prefix = $site->shortname.": ";
201             if ($haserrors) {
202                 $prefix .= "[".strtoupper(get_string('error'))."] ";
203             }
204             $subject = $prefix.get_string('automatedbackupstatus', 'backup');
206             //Send the message
207             $eventdata = new stdClass();
208             $eventdata->modulename        = 'moodle';
209             $eventdata->userfrom          = $admin;
210             $eventdata->userto            = $admin;
211             $eventdata->subject           = $subject;
212             $eventdata->fullmessage       = $message;
213             $eventdata->fullmessageformat = FORMAT_PLAIN;
214             $eventdata->fullmessagehtml   = '';
215             $eventdata->smallmessage      = '';
217             $eventdata->component         = 'moodle';
218             $eventdata->name         = 'backup';
220             message_send($eventdata);
221         }
223         //Everything is finished stop backup_auto_running
224         backup_cron_automated_helper::set_state_running(false);
226         mtrace('Automated backups complete.');
228         return $status;
229     }
231     /**
232      * Gets the results from the last automated backup that was run based upon
233      * the statuses of the courses that were looked at.
234      *
235      * @global moodle_database $DB
236      * @return array
237      */
238     public static function get_backup_status_array() {
239         global $DB;
241         $result = array(
242             self::BACKUP_STATUS_ERROR => 0,
243             self::BACKUP_STATUS_OK => 0,
244             self::BACKUP_STATUS_UNFINISHED => 0,
245             self::BACKUP_STATUS_SKIPPED => 0,
246         );
248         $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) statuscount FROM {backup_courses} bc GROUP BY bc.laststatus');
250         foreach ($statuses as $status) {
251             if (empty($status->statuscount)) {
252                 $status->statuscount = 0;
253             }
254             $result[(int)$status->laststatus] += $status->statuscount;
255         }
257         return $result;
258     }
260     /**
261      * Works out the next time the automated backup should be run.
262      *
263      * @param mixed $timezone
264      * @param int $now
265      * @return int
266      */
267     public static function calculate_next_automated_backup($timezone, $now) {
269         $result = -1;
270         $config = get_config('backup');
271         $midnight = usergetmidnight($now, $timezone);
272         $date = usergetdate($now, $timezone);
274         //Get number of days (from today) to execute backups
275         $automateddays = substr($config->backup_auto_weekdays,$date['wday']) . $config->backup_auto_weekdays;
276         $daysfromtoday = strpos($automateddays, "1");
277         if (empty($daysfromtoday)) {
278             $daysfromtoday = 1;
279         }
281         //If some day has been found
282         if ($daysfromtoday !== false) {
283             //Calculate distance
284             $dist = ($daysfromtoday * 86400) +                //Days distance
285                     ($config->backup_auto_hour * 3600) +      //Hours distance
286                     ($config->backup_auto_minute * 60);       //Minutes distance
287             $result = $midnight + $dist;
288         }
290         //If that time is past, call the function recursively to obtain the next valid day
291         if ($result > 0 && $result < time()) {
292             $result = self::calculate_next_automated_backup($timezone, $result);
293         }
295         return $result;
296     }
298     /**
299      * Launches a automated backup routine for the given course
300      *
301      * @param stdClass $course
302      * @param int $starttime
303      * @param int $userid
304      * @return bool
305      */
306     public static function launch_automated_backup($course, $starttime, $userid) {
308         $config = get_config('backup');
309         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid);
311         try {
313             $settings = array(
314                 'users' => 'backup_auto_users',
315                 'role_assignments' => 'backup_auto_users',
316                 'user_files' => 'backup_auto_user_files',
317                 'activities' => 'backup_auto_activities',
318                 'blocks' => 'backup_auto_blocks',
319                 'filters' => 'backup_auto_filters',
320                 'comments' => 'backup_auto_comments',
321                 'completion_information' => 'backup_auto_userscompletion',
322                 'logs' => 'backup_auto_logs',
323                 'histories' => 'backup_auto_histories'
324             );
325             foreach ($settings as $setting => $configsetting) {
326                 if ($bc->get_plan()->setting_exists($setting)) {
327                     $bc->get_plan()->get_setting($setting)->set_value($config->{$configsetting});
328                 }
329             }
331             // Set the default filename
332             $format = $bc->get_format();
333             $type = $bc->get_type();
334             $id = $bc->get_id();
335             $users = $bc->get_plan()->get_setting('users')->get_value();
336             $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value();
337             $bc->get_plan()->get_setting('filename')->set_value(backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised));
339             $bc->set_status(backup::STATUS_AWAITING);
341             $outcome = $bc->execute_plan();
342             $results = $bc->get_results();
343             $file = $results['backup_destination'];
344             $dir = $config->backup_auto_destination;
345             $storage = (int)$config->backup_auto_storage;
346             if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
347                 $dir = null;
348             }
349             if (!empty($dir) && $storage !== 0) {
350                 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, true);
351                 $outcome = $file->copy_content_to($dir.'/'.$filename);
352                 if ($outcome && $storage === 1) {
353                     $file->delete();
354                 }
355             }
357             $outcome = true;
358         } catch (backup_exception $e) {
359             $bc->log('backup_auto_failed_on_course', backup::LOG_WARNING, $course->shortname);
360             $outcome = false;
361         }
363         $bc->destroy();
364         unset($bc);
366         return true;
367     }
369     /**
370      * Removes deleted courses fromn the backup_courses table so that we don't
371      * waste time backing them up.
372      *
373      * @global moodle_database $DB
374      * @return int
375      */
376     public static function remove_deleted_courses_from_schedule() {
377         global $DB;
378         $skipped = 0;
379         $sql = "SELECT bc.courseid FROM {backup_courses} bc WHERE bc.courseid NOT IN (SELECT c.id FROM {course} c)";
380         $rs = $DB->get_recordset_sql($sql);
381         foreach ($rs as $deletedcourse) {
382             //Doesn't exist, so delete from backup tables
383             $DB->delete_records('backup_courses', array('courseid'=>$deletedcourse->courseid));
384             $skipped++;
385         }
386         $rs->close();
387         return $skipped;
388     }
390     /**
391      * Gets the state of the automated backup system.
392      *
393      * @global moodle_database $DB
394      * @return int One of self::STATE_*
395      */
396     public static function get_automated_backup_state($rundirective = self::RUN_ON_SCHEDULE) {
397         global $DB;
399         $config = get_config('backup');
400         $active = (int)$config->backup_auto_active;
401         if ($active === self::AUTO_BACKUP_DISABLED || ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL)) {
402             return self::STATE_DISABLED;
403         } else if (!empty($config->backup_auto_running)) {
404             // TODO: We should find some way of checking whether the automated
405             // backup has infact finished. In 1.9 this was being done by checking
406             // the log entries.
407             return self::STATE_RUNNING;
408         }
409         return self::STATE_OK;
410     }
412     /**
413      * Sets the state of the automated backup system.
414      *
415      * @param bool $running
416      * @return bool
417      */
418     public static function set_state_running($running = true) {
419         if ($running === true) {
420             if (self::get_automated_backup_state() === self::STATE_RUNNING) {
421                 throw new backup_exception('backup_automated_already_running');
422             }
423             set_config('backup_auto_running', '1', 'backup');
424         } else {
425             unset_config('backup_auto_running', 'backup');
426         }
427         return true;
428     }
430     /**
431      * Removes excess backups from the external system and the local file system.
432      *
433      * The number of backups keep comes from $config->backup_auto_keep
434      *
435      * @param stdClass $course
436      * @return bool
437      */
438     public static function remove_excess_backups($course) {
439         $config = get_config('backup');
440         $keep =     (int)$config->backup_auto_keep;
441         $storage =  $config->backup_auto_storage;
442         $dir =      $config->backup_auto_destination;
444         $backupword = str_replace(' ', '_', moodle_strtolower(get_string('backupfilename')));
445         $backupword = trim(clean_filename($backupword), '_');
447         if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
448             $dir = null;
449         }
451         // Clean up excess backups in the course backup filearea
452         if ($storage == 0 || $storage == 2) {
453             $fs = get_file_storage();
454             $context = get_context_instance(CONTEXT_COURSE, $course->id);
455             $component = 'backup';
456             $filearea = 'automated';
457             $itemid = 0;
458             $files = array();
459             foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
460                 if (strpos($file->get_filename(), $backupword) !== 0) {
461                     continue;
462                 }
463                 $files[$file->get_timemodified()] = $file;
464             }
465             arsort($files);
466             $remove = array_splice($files, $keep);
467             foreach ($remove as $file) {
468                 $file->delete();
469             }
470             //mtrace('Removed '.count($remove).' old backup file(s) from the data directory');
471         }
473         // Clean up excess backups in the specified external directory
474         if (!empty($dir) && ($storage == 1 || $storage == 2)) {
475             // Calculate backup filename regex
477             $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$course->id . '-';
479             $regex = '#^'.preg_quote($filename, '#').'(\d{8})\-(\d{4})\-[a-z]{2}\.mbz$#S';
481             $files = array();
482             foreach (scandir($dir) as $file) {
483                 if (preg_match($regex, $file, $matches)) {
484                     $files[$file] = $matches[1].$matches[2];
485                 }
486             }
487             if (count($files) <= $keep) {
488                 // There are less matching files than the desired number to keep
489                 // do there is nothing to clean up.
490                 return 0;
491             }
492             arsort($files);
493             $remove = array_splice($files, $keep);
494             foreach (array_keys($remove) as $file) {
495                 unlink($dir.'/'.$file);
496             }
497             //mtrace('Removed '.count($remove).' old backup file(s) from external directory');
498         }
500         return true;
501     }