Skip to content

Instantly share code, notes, and snippets.

@ericmerrill
Created October 1, 2019 18:39
Show Gist options
  • Select an option

  • Save ericmerrill/234e18c0022d4bb501cdcfe673c53eb2 to your computer and use it in GitHub Desktop.

Select an option

Save ericmerrill/234e18c0022d4bb501cdcfe673c53eb2 to your computer and use it in GitHub Desktop.
Moodle Backup and Delete Scripts
<?php
define('CLI_SCRIPT', true);
require(dirname(__FILE__).'/config.php');
require_once($CFG->libdir.'/clilib.php'); // cli only functions
// now get cli options
list($options, $unrecognized) = cli_get_params(array('help' => false,
'course' => false,
'cat' => false,
'check-only' => false,
'check-checksum' => false,
'start' => 0,
'limit' => 0,
'no-users' => false,
'force' => false,
'directory' => false),
array('h' => 'help',
'C' => 'course',
'c' => 'cat',
's' => 'start',
'l' => 'limit',
'n' => 'no-users',
'f' => 'force'));
if ($unrecognized) {
$unrecognized = implode("\n ", $unrecognized);
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
}
if (!$options['course'] && !$options['cat']) {
echo "Need to specify a category or course.\n\n";
$options['help'] = true;
}
if ($options['help']) {
$help =
"Execute archive backups.
This script executes archive backups.
Options:
-C, --course Course ID
-c, --cat Category ID
-s, --start Limit start
-l, --limit Limit count
-n, --no-users Create backups without user data
-f, --force Force backup, even if file already exists
--check-only Perform the existing file check only. Don't move or recreate.
--check-checksum When using a .sha1 checkfile to confirm backup has been validated, confirm the checksum value.
--directory Set the absolute directory to place backups in
-h, --help Print out this help
Example:
\$sudo -u www-data /usr/bin/php local/elis/cli/backup_courses.php
";
echo $help;
die;
}
if (CLI_MAINTENANCE) {
echo "CLI maintenance mode active, backup execution suspended.\n";
exit(1);
}
if (moodle_needs_upgrading()) {
echo "Moodle upgrade pending, backup execution suspended.\n";
exit(1);
}
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php');
$DIRECTORY = null;
$starttime = time();
$thisweek = date('Y-m-d');
if ($options['course']) {
if (!is_numeric($options['course'])) {
echo "ERROR: Course ID needs to be a number.\n";
exit(1);
}
$courses = $DB->get_records('course', array('id' => $options['course']));
} else if ($options['cat']) {
if (!is_numeric($options['cat'])) {
echo "ERROR: Category ID needs to be a number.\n";
exit(1);
}
if (!is_numeric($options['start'])) {
echo "ERROR: Limit start needs to be a number.\n";
exit(1);
}
if (!is_numeric($options['limit'])) {
echo "ERROR: Limit count needs to be a number.\n";
exit(1);
}
$courses = $DB->get_records('course', array('category' => $options['cat']), 'id ASC', '*', $options['start'], $options['limit']);
}
if (!$courses) {
echo "ERROR: No courses found.\n";
exit(1);
}
$users = true;
if ($options['no-users']) {
$users = false;
}
setup_config();
$basepath = get_config('backup', 'backup_auto_destination');
$num = 1;
$count = count($courses);
$lf = \core\lock\lock_config::get_lock_factory('local_elis_course_backup');
if ($options['check-only']) {
echo "Performing a check of {$basepath}\n";
foreach ($courses as $course) {
$key = 'course:'.$course->id;
$lock = $lf->get_lock($key, 0);
echo "$num of $count, course ".$course->id.". ";
if ($lock === false) {
echo "Lock held elsewhere.";
} else {
$existing = find_existing_filename($course);
if ($existing !== false) {
echo "{$existing} found. ";
if (check_file_exists($existing)) {
echo "Check file already exists.";
} else {
$good = check_backup_good($existing);
if ($good) {
echo "File is good.";
create_check_file($existing);
} else {
echo "ERROR: Backup file not good.";
}
}
} else {
echo "ERROR: No backup found. ";
}
$lock->release();
}
echo "\n";
$num++;
}
exit();
}
echo "Saving to $basepath\n";
foreach ($courses as $course) {
echo "$num of $count, course ".$course->id.". ";
$key = 'course:'.$course->id;
$lock = $lf->get_lock($key, 0);
if ($lock === false) {
echo "Course lock in place, being backed up elsewhere.";
} else if (($filename = course_exists($course)) !== false) {
echo "Course already exists as ".$filename.".";
$lock->release();
} else {
echo "Backing up ";
if ($status = backup_cron_automated_helper::launch_automated_backup($course, $starttime, 2)) {
$time = time();
$suffix = '-nu';
if ($users) {
$suffix = '';
}
for ($i = 0; $i <= 1200; $i = $i + 60) {
$orgfile = $basepath.'/backup-moodle2-course-'.$course->id.'-'.
date('Ymd', $time-$i).'-'.date('Hi', $time-$i).$suffix.'.mbz';
if (file_exists($orgfile)) {
break;
}
}
$basename = get_filename_base($course);
$name = $basename."-".$thisweek.".mbz";
$newfile = $basepath.'/'.$name;
if (rename($orgfile, $newfile)) {
echo $name.".";
if (!check_backup_good($name)) {
// This shouldn't happen. The file we just made looks like it's bad.
echo "ERROR: The file created seems like it is bad!";
$badfile = $basepath.'/bad-created-'.$name;
rename($newfile, $badfile);
}
} else {
echo "Error renaming.";
}
} else {
echo "Error creating backup.";
}
// Just in case there was some old check file. Remove it.
$checkfile = $basename."-".$thisweek.".sha1";
if (file_exists($basepath.'/'.$checkfile)) {
unlink($basepath.'/'.$checkfile);
}
$lock->release();
}
print "\n";
$num++;
}
function course_exists($course) {
global $basepath, $options;
if ($options['force']) {
return false;
}
$filename = find_existing_filename($course);
if (empty($filename)) {
return false;
}
if (check_file_exists($filename)) {
// The file exists and was already checked.
echo "Checkfile used. ";
return $filename;
} else if (check_backup_good($filename)) {
// Good file, we can use it.
// This is more than the first check, so we can mark it as checked.
echo "File is good. ";
create_check_file($filename);
return $filename;
} else {
// This means we suspect the existing file is bad. Move it, and we will try again.
echo "Moving existing file. ";
$orgfile = $basepath.'/'.$filename;
$newfile = $basepath.'/bad-'.$filename;
rename($orgfile, $newfile);
return false;
}
return false;
}
function find_existing_filename($course) {
global $basepath, $options, $DIRECTORY;
$name = get_filename_base($course);
if (is_null($DIRECTORY)) {
reload_directory();
}
$filename = search_directory_for_file($name);
return $filename;
}
function search_directory_for_file($name, $extension = 'mbz') {
$result = search_directory_for_file_inner($name, $extension);
if ($result !== false) {
return $result;
}
reload_directory();
return search_directory_for_file_inner($name, $extension);
}
function search_directory_for_file_inner($name, $extension = 'mbz') {
global $DIRECTORY;
foreach ($DIRECTORY as $file) {
$ext = $file->getExtension();
if (strcmp($ext, $extension) !== 0) {
continue;
}
$filename = $file->getFilename();
if (strncmp($filename, $name, strlen($name)) === 0) {
return $filename;
}
}
return false;
}
function reload_directory() {
global $basepath, $DIRECTORY;
$DIRECTORY = new DirectoryIterator($basepath);
if (empty($DIRECTORY)) {
echo "ERROR: Cannot open {$basepath}.";
exit(1);
}
}
function find_existing_filename_old($course) {
global $basepath, $options;
$existing = get_directory_list($basepath, '', false, false, true);
if (!($idnumber = $course->idnumber)) {
$idnumber = "noidnum";
}
$name = $idnumber."-".$course->id;
foreach ($existing as $filename) {
if (strncmp($filename, $name, strlen($name)) === 0 && strncmp('mbz', substr($filename, -3), 3) === 0) {
return $filename;
}
}
}
function check_backup_good($backupfile) {
global $basepath, $options, $users;
// A collection of files we expect to be in the complete archive.
// 1 means any filetype, 2 means only in files with user data.
$expected = ['.ARCHIVE_INDEX' => 1,
'badges.xml' => 2,
'completion.xml' => 1,
'course/calendar.xml' => 1,
'course/course.xml' => 1,
'course/comments.xml' => 2,
'course/enrolments.xml' => 1,
'course/filters.xml' => 1,
'course/inforef.xml' => 1,
'course/logs.xml' => 2,
'course/logstores.xml' => 2,
'course/roles.xml' => 1,
'files.xml' => 1,
'grade_history.xml' => 1,
'gradebook.xml' => 1,
'groups.xml' => 1,
'moodle_backup.xml' => 1,
'outcomes.xml' => 1,
'questions.xml' => 1,
'roles.xml' => 1,
'scales.xml' => 1,
'sections/' => 1,
'users.xml' => 2,
'moodle_backup.log' => 1];
if (!$users) {
foreach ($expected as $key => $value) {
if ($value == 2) {
unset($expected[$key]);
}
}
}
$command = 'tar -tf '.escapeshellarg($basepath.'/'.$backupfile).' 2>&1';
exec($command, $output, $returncode);
if ($returncode !== 0) {
echo "ERROR: Bad return code reading file at ".$basepath.'/'.$backupfile.". ";
return false;
}
if (empty($output) || count($output) < 10) {
// BAD!
echo "ERROR: Bad existing file found at ".$basepath.'/'.$backupfile.". ";
return false;
}
// We process through each line of the result, removing it from the expected array.
foreach ($output as $file) {
unset($expected[$file]);
}
// If this array is empty when we are done, then we know we got all the files we expected.
if (empty($expected)) {
return true;
} else {
// There were expected files not found, so we are going to flag this file.
echo "ERROR: Bad existing file found at ".$basepath.'/'.$backupfile.". Missing files: \n "
.implode(array_keys($expected), "\n ");
return false;
}
}
function create_check_file($filename) {
global $basepath;
$sha = sha1_file($basepath.'/'.$filename);
$newname = substr($filename, 0, -3).'sha1';
file_put_contents($basepath.'/'.$newname, $sha);
//touch($basepath.'/'.$newname);
}
function check_file_exists($filename) {
global $basepath, $options;
$checkname = substr($filename, 0, -3).'sha1';
$checkpath = $basepath.'/'.$checkname;
$exists = search_directory_for_file($checkname, 'sha1');
if (!$exists) {
return false;
}
if (empty($options['check-checksum'])) {
return true;
}
// Check the contents of the file.
$fh = fopen($checkpath, 'r');
if (empty($fh)) {
return false;
}
$checksha = trim(fread($fh, 45));
fclose($fp);
if (empty($checksha) || strlen($checksha) !== 40) {
unlink($checkpath);
return false;
}
$newsha = sha1_file($basepath.'/'.$filename);
if (strcmp($checksha, $newsha) === 0) {
// This means the file was good with a matching sha.
return true;
}
// This means the sha checksums didn't match. We should delete it.
unlink($checkpath);
return false;
}
function get_filename_base($course) {
global $users;
if (!($idnumber = $course->idnumber)) {
$idnumber = "noidnum";
}
$name = $idnumber."-".$course->id;
if ($users) {
$name .= '-userdata';
} else {
$name .= '-nousers';
}
return $name;
}
// Force the config for what we are doing.
function setup_config() {
global $CFG, $users, $options;
if ($users) {
$settings = ['backup_auto_storage' => 1,
'backup_auto_users' => true,
'backup_auto_role_assignments' => true,
'backup_auto_activities' => true,
'backup_auto_blocks' => true,
'backup_auto_filters' => true,
'backup_auto_comments' => true,
'backup_auto_badges' => true,
'backup_auto_calendarevents' => true,
'backup_auto_userscompletion' => true,
'backup_auto_logs' => true,
'backup_auto_histories' => true,
'backup_auto_questionbank' => true,
'backup_auto_groups' => true,
'backup_auto_competencies' => true];
} else {
$settings = ['backup_auto_storage' => 1,
'backup_auto_users' => false,
'backup_auto_role_assignments' => false,
'backup_auto_activities' => true,
'backup_auto_blocks' => true,
'backup_auto_filters' => true,
'backup_auto_comments' => false,
'backup_auto_badges' => false,
'backup_auto_calendarevents' => true,
'backup_auto_userscompletion' => false,
'backup_auto_logs' => false,
'backup_auto_histories' => false,
'backup_auto_questionbank' => true,
'backup_auto_groups' => true,
'backup_auto_competencies' => true];
}
if ($options['directory']) {
$dir = $options['directory'];
if (strpos($dir, '/') !== 0) {
echo "ERROR: Directory {$dir} is not absolue - must start with a slash.\n";
exit(1);
}
if (strrpos($dir, '/') === strlen($dir)-1) {
$dir = substr($dir, 0, strlen($dir)-1);
}
if (!is_dir($dir) || !is_writable($dir)) {
echo "ERROR: Directory {$dir} does not exist or is not writable.\n";
exit(1);
}
$settings['backup_auto_destination'] = $dir;
}
if (isset($CFG->forced_plugin_settings['backup'])) {
$CFG->forced_plugin_settings['backup'] = array_merge($CFG->forced_plugin_settings['backup'], $settings);
} else {
$CFG->forced_plugin_settings['backup'] = $settings;
}
}
<?php
define('CLI_SCRIPT', true);
require(dirname(__FILE__).'/config.php');
require_once($CFG->libdir.'/clilib.php'); // cli only functions
// now get cli options
list($options, $unrecognized) = cli_get_params(array('help' => false, 'course' => false, 'cat' => false, 'start' => 0, 'limit' => 0),
array('h' => 'help', 'C' => 'course', 'c' => 'cat', 's' => 'start', 'l' => 'limit'));
if ($unrecognized) {
$unrecognized = implode("\n ", $unrecognized);
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
}
if (!$options['course'] && !$options['cat']) {
echo "Need to specify a category or course.\n\n";
$options['help'] = true;
}
if ($options['help']) {
$help =
"Bulk delete courses - USE WITH CAUTION!
This script bulk deleted courses from Moodle.
Options:
-C, --course Course ID
-c, --cat Category ID
-s, --start Limit start
-l, --limit Limit count
-h, --help Print out this help
Example:
\$sudo -u www-data /usr/bin/php local/elis/cli/delete_category.php
";
echo $help;
die;
}
if (CLI_MAINTENANCE) {
echo "CLI maintenance mode active, backup execution suspended.\n";
exit(1);
}
if (moodle_needs_upgrading()) {
echo "Moodle upgrade pending, backup execution suspended.\n";
exit(1);
}
raise_memory_limit(MEMORY_HUGE);
//$course = $DB->get_record('course', array('id' => 13));
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php');
$starttime = time();
cron_setup_user();
if ($options['course']) {
if (!is_numeric($options['course'])) {
echo "Course ID needs to be a number.\n";
exit(1);
}
$courseids = $DB->get_records('course', array('id' => $options['course']), '', 'id');
} else if ($options['cat']) {
if (!is_numeric($options['cat'])) {
echo "Category ID needs to be a number.\n";
exit(1);
}
if (!is_numeric($options['start'])) {
echo "Limit start needs to be a number.\n";
exit(1);
}
if (!is_numeric($options['limit'])) {
echo "Limit count needs to be a number.\n";
exit(1);
}
$courseids = $DB->get_records('course', array('category' => $options['cat']), 'sortorder ASC', 'id', $options['start'], $options['limit']);
}
if (!$courseids) {
echo "No courses found.\n";
exit(1);
}
$lf = \core\lock\lock_config::get_lock_factory('local_elis_course_delete');
$num = 1;
$count = count($courseids);
foreach ($courseids as $courseid => $unused) {
echo "Deleting $num of $count, course id ".$courseid.". ";
$key = 'course:'.$courseid;
$lock = $lf->get_lock($key, 0);
if ($lock === false) {
echo "Course lock in place, being deleted elsewhere.";
} else {
if ($course = $DB->get_record('course', array('id' => $courseid))) {
if (delete_course($course, false)) {
echo "Successful $course->shortname";
} else {
echo "Error $course->shortname";
}
} else {
echo "Course was already deleted";
}
$lock->release();
}
$num++;
print "\n";
}
fix_course_sortorder();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment