);
$temp->add(new admin_setting_configselect('backup/backup_auto_storage', new lang_string('automatedstorage', 'backup'), new lang_string('automatedstoragehelp', 'backup'), 0, $storageoptions));
$temp->add(new admin_setting_special_backup_auto_destination());
- $keepoptoins = array(
+
+ $maxkeptoptions = array(
0 => new lang_string('all'), 1 => '1',
2 => '2',
5 => '5',
300 => '300',
400 => '400',
500 => '500');
- $temp->add(new admin_setting_configselect('backup/backup_auto_keep', new lang_string('keep'), new lang_string('backupkeephelp'), 1, $keepoptoins));
+ $temp->add(new admin_setting_configselect('backup/backup_auto_max_kept', new lang_string('automatedmaxkept', 'backup'),
+ new lang_string('automatedmaxkepthelp', 'backup'), 1, $maxkeptoptions));
+
+ $automateddeletedaysoptions = array(
+ 0 => new lang_string('never'),
+ 1000 => new lang_string('numdays', '', 1000),
+ 365 => new lang_string('numdays', '', 365),
+ 180 => new lang_string('numdays', '', 180),
+ 150 => new lang_string('numdays', '', 150),
+ 120 => new lang_string('numdays', '', 120),
+ 90 => new lang_string('numdays', '', 90),
+ 60 => new lang_string('numdays', '', 60),
+ 35 => new lang_string('numdays', '', 35),
+ 10 => new lang_string('numdays', '', 10),
+ 5 => new lang_string('numdays', '', 5),
+ 2 => new lang_string('numdays', '', 2)
+ );
+ $temp->add(new admin_setting_configselect('backup/backup_auto_delete_days', new lang_string('automateddeletedays', 'backup'),
+ '', 0, $automateddeletedaysoptions));
+
+ $minkeptoptions = array(
+ 0 => new lang_string('none'),
+ 1 => '1',
+ 2 => '2',
+ 5 => '5',
+ 10 => '10',
+ 20 => '20',
+ 30 => '30',
+ 40 => '40',
+ 50 => '50',
+ 100 => '100',
+ 200 => '200',
+ 300 => '300',
+ 400 => '400'
+ );
+ $temp->add(new admin_setting_configselect('backup/backup_auto_min_kept', new lang_string('automatedminkept', 'backup'),
+ new lang_string('automatedminkepthelp', 'backup'), 0, $minkeptoptions));
+
$temp->add(new admin_setting_configcheckbox('backup/backup_shortname', new lang_string('backup_shortname', 'admin'), new lang_string('backup_shortnamehelp', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_skip_hidden', new lang_string('skiphidden', 'backup'), new lang_string('skiphiddenhelp', 'backup'), 1));
$temp->add(new admin_setting_configselect('backup/backup_auto_skip_modif_days', new lang_string('skipmodifdays', 'backup'), new lang_string('skipmodifdayshelp', 'backup'), 30, array(
const AUTO_BACKUP_ENABLED = 1;
const AUTO_BACKUP_MANUAL = 2;
+ /** Automated backup storage in course backup filearea */
+ const STORAGE_COURSE = 0;
+ /** Automated backup storage in specified directory */
+ const STORAGE_DIRECTORY = 1;
+ /** Automated backup storage in course backup filearea and specified directory */
+ const STORAGE_COURSE_AND_DIRECTORY = 2;
+
/**
* Runs the automated backups if required
*
$backupcourse->nextstarttime = $nextstarttime;
$DB->update_record('backup_courses', $backupcourse);
mtrace('Skipping ' . $course->fullname . ' (Not scheduled for backup until ' . $showtime . ')');
- } else if ($skipped) { // Must have been skipped for a reason.
- $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
- $backupcourse->nextstarttime = $nextstarttime;
- $DB->update_record('backup_courses', $backupcourse);
- mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')');
- mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
} else {
- // Backup every non-skipped courses.
- mtrace('Backing up '.$course->fullname.'...');
+ if ($skipped) { // Must have been skipped for a reason.
+ $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
+ $backupcourse->nextstarttime = $nextstarttime;
+ $DB->update_record('backup_courses', $backupcourse);
+ mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')');
+ mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
+ } else {
+ // Backup every non-skipped courses.
+ mtrace('Backing up '.$course->fullname.'...');
- // We have to send an email because we have included at least one backup.
- $emailpending = true;
+ // We have to send an email because we have included at least one backup.
+ $emailpending = true;
- // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error).
- if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) {
- // Set laststarttime.
- $starttime = time();
+ // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error).
+ if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) {
+ // Set laststarttime.
+ $starttime = time();
- $backupcourse->laststarttime = time();
- $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
- $DB->update_record('backup_courses', $backupcourse);
+ $backupcourse->laststarttime = time();
+ $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
+ $DB->update_record('backup_courses', $backupcourse);
- $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id);
- $backupcourse->lastendtime = time();
- $backupcourse->nextstarttime = $nextstarttime;
+ $backupcourse->laststatus = self::launch_automated_backup($course, $backupcourse->laststarttime,
+ $admin->id);
+ $backupcourse->lastendtime = time();
+ $backupcourse->nextstarttime = $nextstarttime;
- $DB->update_record('backup_courses', $backupcourse);
+ $DB->update_record('backup_courses', $backupcourse);
- if ($backupcourse->laststatus === self::BACKUP_STATUS_OK) {
- // Clean up any excess course backups now that we have
- // taken a successful backup.
- $removedcount = backup_cron_automated_helper::remove_excess_backups($course);
+ mtrace("complete - next execution: $showtime");
}
}
- mtrace("complete - next execution: $showtime");
+ // Remove excess backups.
+ $removedcount = self::remove_excess_backups($course, $now);
}
}
$rs->close();
}
/**
- * Removes excess backups from the external system and the local file system.
+ * Removes excess backups from a specified course.
*
- * The number of backups keep comes from $config->backup_auto_keep.
- *
- * @param stdClass $course object
- * @return bool
+ * @param stdClass $course Course object
+ * @param int $now Starting time of the process
+ * @return bool Whether or not backups is being removed
*/
- public static function remove_excess_backups($course) {
+ public static function remove_excess_backups($course, $now = null) {
$config = get_config('backup');
- $keep = (int)$config->backup_auto_keep;
- $storage = $config->backup_auto_storage;
- $dir = $config->backup_auto_destination;
+ $maxkept = (int)$config->backup_auto_max_kept;
+ $storage = $config->backup_auto_storage;
+ $deletedays = (int)$config->backup_auto_delete_days;
- if ($keep == 0) {
- // Means keep all backup files.
+ if ($maxkept == 0 && $deletedays == 0) {
+ // Means keep all backup files and never delete backup after x days.
return true;
}
- if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
- $dir = null;
+ if (!isset($now)) {
+ $now = time();
}
// Clean up excess backups in the course backup filearea.
- if ($storage == 0 || $storage == 2) {
- $fs = get_file_storage();
- $context = context_course::instance($course->id);
- $component = 'backup';
- $filearea = 'automated';
- $itemid = 0;
- $files = array();
- // Store all the matching files into timemodified => stored_file array.
- foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
- $files[$file->get_timemodified()] = $file;
+ $deletedcoursebackups = false;
+ if ($storage == self::STORAGE_COURSE || $storage == self::STORAGE_COURSE_AND_DIRECTORY) {
+ $deletedcoursebackups = self::remove_excess_backups_from_course($course, $now);
+ }
+
+ // Clean up excess backups in the specified external directory.
+ $deleteddirectorybackups = false;
+ if ($storage == self::STORAGE_DIRECTORY || $storage == self::STORAGE_COURSE_AND_DIRECTORY) {
+ $deleteddirectorybackups = self::remove_excess_backups_from_directory($course, $now);
+ }
+
+ if ($deletedcoursebackups || $deleteddirectorybackups) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes excess backups in the course backup filearea from a specified course.
+ *
+ * @param stdClass $course Course object
+ * @param int $now Starting time of the process
+ * @return bool Whether or not backups are being removed
+ */
+ protected static function remove_excess_backups_from_course($course, $now) {
+ $fs = get_file_storage();
+ $context = context_course::instance($course->id);
+ $component = 'backup';
+ $filearea = 'automated';
+ $itemid = 0;
+ $backupfiles = array();
+ $backupfilesarea = $fs->get_area_files($context->id, $component, $filearea, $itemid, 'timemodified DESC', false);
+ // Store all the matching files into timemodified => stored_file array.
+ foreach ($backupfilesarea as $backupfile) {
+ $backupfiles[$backupfile->get_timemodified()] = $backupfile;
+ }
+
+ $backupstodelete = self::get_backups_to_delete($backupfiles, $now);
+ if ($backupstodelete) {
+ foreach ($backupstodelete as $backuptodelete) {
+ $backuptodelete->delete();
}
- if (count($files) <= $keep) {
- // There are less matching files than the desired number to keep there is nothing to clean up.
- return 0;
+ mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from the automated filearea');
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes excess backups in the specified external directory from a specified course.
+ *
+ * @param stdClass $course Course object
+ * @param int $now Starting time of the process
+ * @return bool Whether or not backups are being removed
+ */
+ protected static function remove_excess_backups_from_directory($course, $now) {
+ $config = get_config('backup');
+ $dir = $config->backup_auto_destination;
+
+ $isnotvaliddir = !file_exists($dir) || !is_dir($dir) || !is_writable($dir);
+ if ($isnotvaliddir) {
+ mtrace('Error: ' . $dir . ' does not appear to be a valid directory');
+ return false;
+ }
+
+ // Calculate backup filename regex, ignoring the date/time/info parts that can be
+ // variable, depending of languages, formats and automated backup settings.
+ $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
+ $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
+
+ // Store all the matching files into filename => timemodified array.
+ $backupfiles = array();
+ foreach (scandir($dir) as $backupfile) {
+ // Skip files not matching the naming convention.
+ if (!preg_match($regex, $backupfile)) {
+ continue;
}
- // Sort by keys descending (newer to older filemodified).
- krsort($files);
- $remove = array_splice($files, $keep);
- foreach ($remove as $file) {
- $file->delete();
+
+ // Read the information contained in the backup itself.
+ try {
+ $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $backupfile);
+ } catch (backup_helper_exception $e) {
+ mtrace('Error: ' . $backupfile . ' does not appear to be a valid backup (' . $e->errorcode . ')');
+ continue;
}
- //mtrace('Removed '.count($remove).' old backup file(s) from the automated filearea');
- }
- // Clean up excess backups in the specified external directory.
- if (!empty($dir) && ($storage == 1 || $storage == 2)) {
- // Calculate backup filename regex, ignoring the date/time/info parts that can be
- // variable, depending of languages, formats and automated backup settings.
- $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
- $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
-
- // Store all the matching files into filename => timemodified array.
- $files = array();
- foreach (scandir($dir) as $file) {
- // Skip files not matching the naming convention.
- if (!preg_match($regex, $file, $matches)) {
- continue;
- }
+ // Make sure this backup concerns the course and site we are looking for.
+ if ($bcinfo->format === backup::FORMAT_MOODLE &&
+ $bcinfo->type === backup::TYPE_1COURSE &&
+ $bcinfo->original_course_id == $course->id &&
+ backup_general_helper::backup_is_samesite($bcinfo)) {
+ $backupfiles[$bcinfo->backup_date] = $backupfile;
+ }
+ }
- // Read the information contained in the backup itself.
- try {
- $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $file);
- } catch (backup_helper_exception $e) {
- mtrace('Error: ' . $file . ' does not appear to be a valid backup (' . $e->errorcode . ')');
- continue;
- }
+ $backupstodelete = self::get_backups_to_delete($backupfiles, $now);
+ if ($backupstodelete) {
+ foreach ($backupstodelete as $backuptodelete) {
+ unlink($dir . '/' . $backuptodelete);
+ }
+ mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from external directory');
+ return true;
+ } else {
+ return false;
+ }
+ }
- // Make sure this backup concerns the course and site we are looking for.
- if ($bcinfo->format === backup::FORMAT_MOODLE &&
- $bcinfo->type === backup::TYPE_1COURSE &&
- $bcinfo->original_course_id == $course->id &&
- backup_general_helper::backup_is_samesite($bcinfo)) {
- $files[$file] = $bcinfo->backup_date;
+ /**
+ * Get the list of backup files to delete depending on the automated backup settings.
+ *
+ * @param array $backupfiles Existing backup files
+ * @param int $now Starting time of the process
+ * @return array Backup files to delete
+ */
+ protected static function get_backups_to_delete($backupfiles, $now) {
+ $config = get_config('backup');
+ $maxkept = (int)$config->backup_auto_max_kept;
+ $deletedays = (int)$config->backup_auto_delete_days;
+ $minkept = (int)$config->backup_auto_min_kept;
+
+ // Sort by keys descending (newer to older filemodified).
+ krsort($backupfiles);
+ $tokeep = $maxkept;
+ if ($deletedays > 0) {
+ $deletedayssecs = $deletedays * DAYSECS;
+ $tokeep = 0;
+ $backupfileskeys = array_keys($backupfiles);
+ foreach ($backupfileskeys as $timemodified) {
+ $mustdeletebackup = $timemodified < ($now - $deletedayssecs);
+ if ($mustdeletebackup || $tokeep >= $maxkept) {
+ break;
}
+ $tokeep++;
}
- if (count($files) <= $keep) {
- // There are less matching files than the desired number to keep there is nothing to clean up.
- return 0;
- }
- // Sort by values descending (newer to older filemodified).
- arsort($files);
- $remove = array_splice($files, $keep);
- foreach (array_keys($remove) as $file) {
- unlink($dir . '/' . $file);
+
+ if ($tokeep < $minkept) {
+ $tokeep = $minkept;
}
- //mtrace('Removed '.count($remove).' old backup file(s) from external directory');
}
- return true;
+ if (count($backupfiles) <= $tokeep) {
+ // There are less or equal matching files than the desired number to keep, there is nothing to clean up.
+ return false;
+ } else {
+ $backupstodelete = array_splice($backupfiles, $tokeep);
+ return $backupstodelete;
+ }
}
/**
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-20:00'), date('w-H:i', $next));
}
+
+ /**
+ * Test {@link backup_cron_automated_helper::get_backups_to_delete}.
+ */
+ public function test_get_backups_to_delete() {
+ $this->resetAfterTest();
+ // Active only backup_auto_max_kept config to 2 days.
+ set_config('backup_auto_max_kept', '2', 'backup');
+ set_config('backup_auto_delete_days', '0', 'backup');
+ set_config('backup_auto_min_kept', '0', 'backup');
+
+ // No backups to delete.
+ $backupfiles = array(
+ '1000000000' => 'file1.mbz',
+ '1000432000' => 'file3.mbz'
+ );
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000);
+ $this->assertFalse($deletedbackups);
+
+ // Older backup to delete.
+ $backupfiles['1000172800'] = 'file2.mbz';
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000);
+ $this->assertEquals(1, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+
+ // Activate backup_auto_max_kept to 5 days and backup_auto_delete_days to 10 days.
+ set_config('backup_auto_max_kept', '5', 'backup');
+ set_config('backup_auto_delete_days', '10', 'backup');
+ set_config('backup_auto_min_kept', '0', 'backup');
+
+ // No backups to delete. Timestamp is 1000000000 + 10 days.
+ $backupfiles['1000432001'] = 'file4.mbz';
+ $backupfiles['1000864000'] = 'file5.mbz';
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864000);
+ $this->assertFalse($deletedbackups);
+
+ // One old backup to delete. Timestamp is 1000000000 + 10 days + 1 second.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864001);
+ $this->assertEquals(1, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+
+ // Two old backups to delete. Timestamp is 1000000000 + 12 days + 1 second.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001036801);
+ $this->assertEquals(2, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+ $this->assertArrayHasKey('1000172800', $backupfiles);
+ $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+
+ // Activate backup_auto_max_kept to 5 days, backup_auto_delete_days to 10 days and backup_auto_min_kept to 2.
+ set_config('backup_auto_max_kept', '5', 'backup');
+ set_config('backup_auto_delete_days', '10', 'backup');
+ set_config('backup_auto_min_kept', '2', 'backup');
+
+ // Three instead of four old backups are deleted. Timestamp is 1000000000 + 16 days.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001382400);
+ $this->assertEquals(3, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+ $this->assertArrayHasKey('1000172800', $backupfiles);
+ $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+ $this->assertArrayHasKey('1000432000', $backupfiles);
+ $this->assertEquals('file3.mbz', $backupfiles['1000432000']);
+
+ // Three instead of all five backups are deleted. Timestamp is 1000000000 + 60 days.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1005184000);
+ $this->assertEquals(3, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+ $this->assertArrayHasKey('1000172800', $backupfiles);
+ $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+ $this->assertArrayHasKey('1000432000', $backupfiles);
+ $this->assertEquals('file3.mbz', $backupfiles['1000432000']);
+ }
+}
+
+/**
+ * Provides access to protected methods we want to explicitly test
+ *
+ * @copyright 2015 Jean-Philippe Gaudreau <jp.gaudreau@umontreal.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_backup_cron_automated_helper extends backup_cron_automated_helper {
+
+ /**
+ * Provides access to protected method get_backups_to_remove.
+ *
+ * @param array $backupfiles Existing backup files
+ * @param int $now Starting time of the process
+ * @return array Backup files to remove
+ */
+ public static function testable_get_backups_to_delete($backupfiles, $now) {
+ return parent::get_backups_to_delete($backupfiles, $now);
+ }
}