source: moodle/trunk/fuentes/backup/util/helper/backup_cron_helper.class.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 31.2 KB
Line 
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
27defined('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 */
33abstract 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}
Note: See TracBrowser for help on using the repository browser.