MDL-28505 Backup: Asynchronous backup and restore

This patch adds asynchronous backup and restore functionality
to Moodle. This is an optional feature and is not enabled by
default. It can be enabled by site administrators.
Asynchronous backup and restores are actioned by the Moodle
adhoc task API. The progress of backup and restores is
displayedin the Moodle UI.  Users can also be sent a message
when a backup or restore operation completes via the
Moodle messaging API.
This commit is contained in:
Matt Porritt
2019-04-09 00:15:42 +00:00
parent 9d4f4f0051
commit 2cd901a4e7
35 changed files with 2459 additions and 115 deletions
+24
View File
@@ -458,4 +458,28 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
$ADMIN->add('backups', $temp);
// Create a page for asynchronous backup and restore configuration and defaults.
if (!empty($CFG->enableasyncbackup)) { // Only add settings if async mode is enable at site level.
$temp = new admin_settingpage('asyncgeneralsettings', new lang_string('asyncgeneralsettings', 'backup'));
$temp->add(new admin_setting_configcheckbox(
'backup/backup_async_message_users',
new lang_string('asyncemailenable', 'backup'),
new lang_string('asyncemailenabledetail', 'backup'), 0));
$temp->add(new admin_setting_configtext(
'backup/backup_async_message_subject',
new lang_string('asyncmessagesubject', 'backup'),
new lang_string('asyncmessagesubjectdetail', 'backup'),
new lang_string('asyncmessagesubjectdefault', 'backup')));
$temp->add(new admin_setting_confightmleditor(
'backup/backup_async_message',
new lang_string('asyncmessagebody', 'backup'),
new lang_string('asyncmessagebodydetail', 'backup'),
new lang_string('asyncmessagebodydefault', 'backup')));
$ADMIN->add('backups', $temp);
}
}
+3
View File
@@ -51,4 +51,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
$optionalsubsystems->add(new admin_setting_configcheckbox('enablecoursepublishing',
new lang_string('enablecoursepublishing', 'hub'), new lang_string('enablecoursepublishing_help', 'hub'), 0));
$optionalsubsystems->add(new admin_setting_configcheckbox('enableasyncbackup', new lang_string('enableasyncbackup', 'backup'),
new lang_string('enableasyncbackup_help', 'backup'), 0, 1, 0));
}
+6
View File
@@ -74,6 +74,12 @@ abstract class backup implements checksumable {
const MODE_AUTOMATED = 50;
const MODE_CONVERTED = 60;
/**
* This mode is for asynchronous backups.
* These backups will run via adhoc scheduled tasks.
*/
const MODE_ASYNC = 70;
// Target (new/existing/current/adding/deleting)
const TARGET_CURRENT_DELETING = 0;
const TARGET_CURRENT_ADDING = 1;
+129 -74
View File
@@ -30,16 +30,27 @@ require_once('../config.php');
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
// Backup of large courses requires extra memory. Use the amount configured
// in admin settings.
raise_memory_limit(MEMORY_EXTRA);
$courseid = required_param('id', PARAM_INT);
$sectionid = optional_param('section', null, PARAM_INT);
$cmid = optional_param('cm', null, PARAM_INT);
$cancel = optional_param('cancel', '', PARAM_ALPHA);
$previous = optional_param('previous', false, PARAM_BOOL);
/**
* Part of the forms in stages after initial, is POST never GET
*/
$backupid = optional_param('backup', false, PARAM_ALPHANUM);
// Determine if we are performing realtime for asynchronous backups.
$backupmode = backup::MODE_GENERAL;
if (async_helper::is_async_enabled()) {
$backupmode = backup::MODE_ASYNC;
}
$courseurl = new moodle_url('/course/view.php', array('id' => $courseid));
$url = new moodle_url('/backup/backup.php', array('id'=>$courseid));
if ($sectionid !== null) {
$url->param('section', $sectionid);
@@ -53,6 +64,8 @@ $PAGE->set_pagelayout('admin');
$id = $courseid;
$cm = null;
$course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
$coursecontext = context_course::instance($course->id);
$contextid = $coursecontext->id;
$type = backup::TYPE_1COURSE;
if (!is_null($sectionid)) {
$section = $DB->get_record('course_sections', array('course'=>$course->id, 'id'=>$sectionid), '*', MUST_EXIST);
@@ -68,11 +81,10 @@ require_login($course, false, $cm);
switch ($type) {
case backup::TYPE_1COURSE :
require_capability('moodle/backup:backupcourse', context_course::instance($course->id));
require_capability('moodle/backup:backupcourse', $coursecontext);
$heading = get_string('backupcourse', 'backup', $course->shortname);
break;
case backup::TYPE_1SECTION :
$coursecontext = context_course::instance($course->id);
require_capability('moodle/backup:backupsection', $coursecontext);
if ((string)$section->name !== '') {
$sectionname = format_string($section->name, true, array('context' => $coursecontext));
@@ -84,102 +96,145 @@ switch ($type) {
}
break;
case backup::TYPE_1ACTIVITY :
require_capability('moodle/backup:backupactivity', context_module::instance($cm->id));
$activitycontext = context_module::instance($cm->id);
require_capability('moodle/backup:backupactivity', $activitycontext);
$contextid = $activitycontext->id;
$heading = get_string('backupactivity', 'backup', $cm->name);
break;
default :
print_error('unknownbackuptype');
}
// Backup of large courses requires extra memory. Use the amount configured
// in admin settings.
raise_memory_limit(MEMORY_EXTRA);
if (!($bc = backup_ui::load_controller($backupid))) {
$bc = new backup_controller($type, $id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_YES, backup::MODE_GENERAL, $USER->id);
}
$backup = new backup_ui($bc);
$PAGE->set_title($heading);
$PAGE->set_heading($heading);
$renderer = $PAGE->get_renderer('core','backup');
if (empty($cancel)) {
// Do not print the header if user cancelled the process, as we are going to redirect the user.
echo $OUTPUT->header();
}
// Prepare a progress bar which can display optionally during long-running
// operations while setting up the UI.
$slowprogress = new \core\progress\display_if_slow(get_string('preparingui', 'backup'));
// Only let user perform a backup if we aren't in async mode, or if we are
// and there are no pending backups for this item for this user.
if (!async_helper::is_async_pending($id, 'course', 'backup')) {
$previous = optional_param('previous', false, PARAM_BOOL);
if ($backup->get_stage() == backup_ui::STAGE_SCHEMA && !$previous) {
// After schema stage, we are probably going to get to the confirmation stage,
// The confirmation stage has 2 sets of progress, so this is needed to prevent
// it showing 2 progress bars.
$twobars = true;
$slowprogress->start_progress('', 2);
} else {
$twobars = false;
}
$backup->get_controller()->set_progress($slowprogress);
$backup->process();
// The mix of business logic and display elements below makes me sad.
// This needs to refactored into the renderer and seperated out.
if ($backup->enforce_changed_dependencies()) {
debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER);
}
$loghtml = '';
if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
// Display an extra backup step bar so that we can show the 'processing' step first.
echo html_writer::start_div('', array('id' => 'executionprogress'));
echo $renderer->progress_bar($backup->get_progress_bar());
$backup->get_controller()->set_progress(new \core\progress\display());
// Prepare logger and add to end of chain.
$logger = new core_backup_html_logger($CFG->debugdeveloper ? backup::LOG_DEBUG : backup::LOG_INFO);
$backup->get_controller()->add_logger($logger);
// Carry out actual backup.
$backup->execute();
// Backup controller gets saved/loaded so the logger object changes and we
// have to retrieve it.
$logger = $backup->get_controller()->get_logger();
while (!is_a($logger, 'core_backup_html_logger')) {
$logger = $logger->get_next();
if (!($bc = backup_ui::load_controller($backupid))) {
$bc = new backup_controller($type, $id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_YES, $backupmode, $USER->id);
}
// Get HTML from logger.
if ($CFG->debugdisplay) {
$loghtml = $logger->get_html();
// Prepare a progress bar which can display optionally during long-running
// operations while setting up the UI.
$slowprogress = new \core\progress\display_if_slow(get_string('preparingui', 'backup'));
$renderer = $PAGE->get_renderer('core', 'backup');
$backup = new backup_ui($bc);
if ($backup->get_stage() == backup_ui::STAGE_SCHEMA && !$previous) {
// After schema stage, we are probably going to get to the confirmation stage,
// The confirmation stage has 2 sets of progress, so this is needed to prevent
// it showing 2 progress bars.
$twobars = true;
$slowprogress->start_progress('', 2);
} else {
$twobars = false;
}
$backup->get_controller()->set_progress($slowprogress);
$backup->process();
if ($backup->enforce_changed_dependencies()) {
debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER);
}
// Hide the progress display and first backup step bar (the 'finished' step will show next).
echo html_writer::end_div();
echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
} else {
$backup->save_controller();
}
$loghtml = '';
if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
// Displaying UI can require progress reporting, so do it here before outputting
// the backup stage bar (as part of the existing progress bar, if required).
$ui = $backup->display($renderer);
if ($twobars) {
$slowprogress->end_progress();
}
if ($backupmode != backup::MODE_ASYNC) {
// Synchronous backup handling.
echo $renderer->progress_bar($backup->get_progress_bar());
// Display an extra backup step bar so that we can show the 'processing' step first.
echo html_writer::start_div('', array('id' => 'executionprogress'));
echo $renderer->progress_bar($backup->get_progress_bar());
$backup->get_controller()->set_progress(new \core\progress\display());
echo $ui;
$backup->destroy();
unset($backup);
// Prepare logger and add to end of chain.
$logger = new core_backup_html_logger($CFG->debugdeveloper ? backup::LOG_DEBUG : backup::LOG_INFO);
$backup->get_controller()->add_logger($logger);
// Display log data if there was any.
if ($loghtml != '') {
echo $renderer->log_display($loghtml);
// Carry out actual backup.
$backup->execute();
// Backup controller gets saved/loaded so the logger object changes and we
// have to retrieve it.
$logger = $backup->get_controller()->get_logger();
while (!is_a($logger, 'core_backup_html_logger')) {
$logger = $logger->get_next();
}
// Get HTML from logger.
if ($CFG->debugdisplay) {
$loghtml = $logger->get_html();
}
// Hide the progress display and first backup step bar (the 'finished' step will show next).
echo html_writer::end_div();
echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
} else {
// Async backup handling.
$backup->get_controller()->finish_ui();
echo html_writer::start_div('', array('id' => 'executionprogress'));
echo $renderer->progress_bar($backup->get_progress_bar());
echo html_writer::end_div();
// Create adhoc task for backup.
$asynctask = new \core\task\asynchronous_backup_task();
$asynctask->set_blocking(false);
$asynctask->set_custom_data(array('backupid' => $backupid));
\core\task\manager::queue_adhoc_task($asynctask);
// Add ajax progress bar and initiate ajax via a template.
$restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid));
$progresssetup = array(
'backupid' => $backupid,
'contextid' => $contextid,
'courseurl' => $courseurl->out(),
'restoreurl' => $restoreurl->out(),
'headingident' => 'backup'
);
echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
}
} else {
$backup->save_controller();
}
if ($backup->get_stage() != backup_ui::STAGE_FINAL) {
// Displaying UI can require progress reporting, so do it here before outputting
// the backup stage bar (as part of the existing progress bar, if required).
$ui = $backup->display($renderer);
if ($twobars) {
$slowprogress->end_progress();
}
echo $renderer->progress_bar($backup->get_progress_bar());
echo $ui;
// Display log data if there was any.
if ($loghtml != '' && $backupmode != backup::MODE_ASYNC) {
echo $renderer->log_display($loghtml);
}
}
$backup->destroy();
unset($backup);
} else { // User has a pending async operation.
echo $OUTPUT->notification(get_string('pendingasyncerror', 'backup'), 'error');
echo $OUTPUT->container(get_string('pendingasyncdetail', 'backup'));
echo $OUTPUT->continue_button($courseurl);
}
echo $OUTPUT->footer();
+25 -9
View File
@@ -58,7 +58,11 @@ class backup_controller extends base_controller {
protected $plan; // Backup execution plan
protected $includefiles; // Whether this backup includes files or not.
protected $execution; // inmediate/delayed
/**
* Immediate/delayed execution type.
* @var integer
*/
protected $execution;
protected $executiontime; // epoch time when we want the backup to be executed (requires cron to run)
protected $destination; // Destination chain object (fs_moodle, fs_os, db, email...)
@@ -85,11 +89,17 @@ class backup_controller extends base_controller {
$this->userid = $userid;
// Apply some defaults
$this->execution = backup::EXECUTION_INMEDIATE;
$this->operation = backup::OPERATION_BACKUP;
$this->executiontime = 0;
$this->checksum = '';
// Set execution based on backup mode.
if ($mode == backup::MODE_ASYNC) {
$this->execution = backup::EXECUTION_DELAYED;
} else {
$this->execution = backup::EXECUTION_INMEDIATE;
}
// Apply current backup version and release if necessary
backup_controller_dbops::apply_version_and_release();
@@ -112,7 +122,7 @@ class backup_controller extends base_controller {
// display progress must set it.
$this->progress = new \core\progress\none();
// Instantiate the output_controller singleton and active it if interactive and inmediate
// Instantiate the output_controller singleton and active it if interactive and immediate.
$oc = output_controller::get_instance();
if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) {
$oc->set_active(true);
@@ -182,7 +192,8 @@ class backup_controller extends base_controller {
// TODO: Check it's a correct status.
$this->status = $status;
// Ensure that, once set to backup::STATUS_AWAITING, controller is stored in DB.
if ($status == backup::STATUS_AWAITING) {
// Also save if executing so we can better track progress.
if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_EXECUTING) {
$this->save_controller();
$tbc = self::load_controller($this->backupid);
$this->logger = $tbc->logger; // wakeup loggers
@@ -192,14 +203,18 @@ class backup_controller extends base_controller {
// If the operation has ended without error (backup::STATUS_FINISHED_OK)
// proceed by cleaning the object from database. MDL-29262.
$this->save_controller(false, true);
} else if ($status == backup::STATUS_FINISHED_ERR) {
// If the operation has ended with an error save the controller
// preserving the object in the database. We may want it for debugging.
$this->save_controller();
}
}
public function set_execution($execution, $executiontime = 0) {
$this->log('setting controller execution', backup::LOG_DEBUG);
// TODO: Check valid execution mode
// TODO: Check time in future
// TODO: Check time = 0 if inmediate
// TODO: Check valid execution mode.
// TODO: Check time in future.
// TODO: Check time = 0 if immediate.
$this->execution = $execution;
$this->executiontime = $executiontime;
@@ -333,8 +348,8 @@ class backup_controller extends base_controller {
* @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false)
*/
public function save_controller($includeobj = true, $cleanobj = false) {
// Going to save controller to persistent storage, calculate checksum for later checks and save it
// TODO: flag the controller as NA. Any operation on it should be forbidden util loaded back
// Going to save controller to persistent storage, calculate checksum for later checks and save it.
// TODO: flag the controller as NA. Any operation on it should be forbidden until loaded back.
$this->log('saving controller to db', backup::LOG_DEBUG);
if ($includeobj ) { // Only calculate checksum if we are going to include the object.
$this->checksum = $this->calculate_checksum();
@@ -399,6 +414,7 @@ class backup_controller extends base_controller {
$this->log("setting file inclusion to {$this->includefiles}", backup::LOG_DEBUG);
return $this->includefiles;
}
}
/*
+22 -7
View File
@@ -53,7 +53,11 @@ class restore_controller extends base_controller {
/** @var restore_plan */
protected $plan; // Restore execution plan
protected $execution; // inmediate/delayed
/**
* Immediate/delayed execution type.
* @var integer
*/
protected $execution;
protected $executiontime; // epoch time when we want the restore to be executed (requires cron to run)
protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses
@@ -88,7 +92,6 @@ class restore_controller extends base_controller {
// Apply some defaults
$this->type = '';
$this->format = backup::FORMAT_UNKNOWN;
$this->execution = backup::EXECUTION_INMEDIATE;
$this->operation = backup::OPERATION_RESTORE;
$this->executiontime = 0;
$this->samesite = false;
@@ -110,6 +113,13 @@ class restore_controller extends base_controller {
// Default logger chain (based on interactive/execution)
$this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid);
// Set execution based on backup mode.
if ($mode == backup::MODE_ASYNC) {
$this->execution = backup::EXECUTION_DELAYED;
} else {
$this->execution = backup::EXECUTION_INMEDIATE;
}
// By default there is no progress reporter unless you specify one so it
// can be used during loading of the plan.
if ($progress) {
@@ -119,7 +129,7 @@ class restore_controller extends base_controller {
}
$this->progress->start_progress('Constructing restore_controller');
// Instantiate the output_controller singleton and active it if interactive and inmediate
// Instantiate the output_controller singleton and active it if interactive and immediate.
$oc = output_controller::get_instance();
if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) {
$oc->set_active(true);
@@ -198,7 +208,8 @@ class restore_controller extends base_controller {
// TODO: Check it's a correct status.
$this->status = $status;
// Ensure that, once set to backup::STATUS_AWAITING | STATUS_NEED_PRECHECK, controller is stored in DB.
if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_NEED_PRECHECK) {
// Also save if executing so we can better track progress.
if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_NEED_PRECHECK || $status == backup::STATUS_EXECUTING) {
$this->save_controller();
$tbc = self::load_controller($this->restoreid);
$this->logger = $tbc->logger; // wakeup loggers
@@ -208,14 +219,18 @@ class restore_controller extends base_controller {
// If the operation has ended without error (backup::STATUS_FINISHED_OK)
// proceed by cleaning the object from database. MDL-29262.
$this->save_controller(false, true);
} else if ($status == backup::STATUS_FINISHED_ERR) {
// If the operation has ended with an error save the controller
// preserving the object in the database. We may want it for debugging.
$this->save_controller();
}
}
public function set_execution($execution, $executiontime = 0) {
$this->log('setting controller execution', backup::LOG_DEBUG);
// TODO: Check valid execution mode
// TODO: Check time in future
// TODO: Check time = 0 if inmediate
// TODO: Check valid execution mode.
// TODO: Check time in future.
// TODO: Check time = 0 if immediate.
$this->execution = $execution;
$this->executiontime = $executiontime;
+240
View File
@@ -0,0 +1,240 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External backup API.
*
* @package core_backup
* @category external
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
require_once("$CFG->libdir/externallib.php");
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Backup external functions.
*
* @package core_backup
* @category external
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.7
*/
class core_backup_external extends external_api {
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.7
*/
public static function get_async_backup_progress_parameters() {
return new external_function_parameters(
array(
'backupids' => new external_multiple_structure(
new external_value(PARAM_ALPHANUM, 'Backup id to get progress for', VALUE_REQUIRED, null, NULL_ALLOWED),
'Backup id to get progress for', VALUE_REQUIRED
),
'contextid' => new external_value(PARAM_INT, 'Context id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
)
);
}
/**
* Get asynchronous backup progress.
*
* @param string $backupids The ids of the backup to get progress for.
* @param int $contextid The context the backup relates to.
* @return array $results The array of results.
* @since Moodle 3.7
*/
public static function get_async_backup_progress($backupids, $contextid) {
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
// Parameter validation.
self::validate_parameters(
self::get_async_backup_progress_parameters(),
array(
'backupids' => $backupids,
'contextid' => $contextid
)
);
// Context validation.
list($context, $course, $cm) = get_context_info_array($contextid);
self::validate_context($context);
if ($cm) {
require_capability('moodle/backup:backupactivity', $context);
} else {
require_capability('moodle/backup:backupcourse', $context);
$instanceid = $course->id;
}
$results = array();
foreach ($backupids as $backupid) {
$results[] = backup_controller_dbops::get_progress($backupid);
}
return $results;
}
/**
* Returns description of method result value
*
* @return external_description
* @since Moodle 3.7
*/
public static function get_async_backup_progress_returns() {
return new external_multiple_structure(
new external_single_structure(
array(
'status' => new external_value(PARAM_INT, 'Backup Status'),
'progress' => new external_value(PARAM_FLOAT, 'Backup progress'),
'backupid' => new external_value(PARAM_ALPHANUM, 'Backup id'),
'operation' => new external_value(PARAM_ALPHANUM, 'operation type'),
), 'Backup completion status'
), 'Backup data'
);
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.7
*/
public static function get_async_backup_links_backup_parameters() {
return new external_function_parameters(
array(
'filename' => new external_value(PARAM_FILE, 'Backup filename', VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
'contextid' => new external_value(PARAM_INT, 'Context id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
)
);
}
/**
* Get the data to be used when generating the table row for an asynchronous backup,
* the table row updates via ajax when backup is complete.
*
* @param string $filename The file name of the backup file.
* @param int $contextid The context the backup relates to.
* @since Moodle 3.7
*/
public static function get_async_backup_links_backup($filename, $contextid) {
// Parameter validation.
self::validate_parameters(
self::get_async_backup_links_backup_parameters(),
array(
'filename' => $filename,
'contextid' => $contextid
)
);
// Context validation.
list($context, $course, $cm) = get_context_info_array($contextid);
self::validate_context($context);
require_capability('moodle/backup:backupcourse', $context);
if ($cm) {
$filearea = 'activity';
} else {
$filearea = 'course';
}
$results = \async_helper::get_backup_file_info($filename, $filearea, $contextid);
return $results;
}
/**
* Returns description of method result value.
*
* @return external_description
* @since Moodle 3.7
*/
public static function get_async_backup_links_backup_returns() {
return new external_single_structure(
array(
'filesize' => new external_value(PARAM_TEXT, 'Backup file size'),
'fileurl' => new external_value(PARAM_URL, 'Backup file URL'),
'restoreurl' => new external_value(PARAM_URL, 'Backup restore URL'),
), 'Table row data.');
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.7
*/
public static function get_async_backup_links_restore_parameters() {
return new external_function_parameters(
array(
'backupid' => new external_value(PARAM_ALPHANUMEXT, 'Backup id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
'contextid' => new external_value(PARAM_INT, 'Context id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
)
);
}
/**
* Get the data to be used when generating the table row for an asynchronous restore,
* the table row updates via ajax when restore is complete.
*
* @param string $backupid The id of the backup record.
* @param int $contextid The context the restore relates to.
* @return array $results The array of results.
* @since Moodle 3.7
*/
public static function get_async_backup_links_restore($backupid, $contextid) {
// Parameter validation.
self::validate_parameters(
self::get_async_backup_links_restore_parameters(),
array(
'backupid' => $backupid,
'contextid' => $contextid
)
);
// Context validation.
$context = context::instance_by_id($contextid);
self::validate_context($context);
require_capability('moodle/restore:restorecourse', $context);
$results = \async_helper::get_restore_url($backupid);
return $results;
}
/**
* Returns description of method result value.
*
* @return external_description
* @since Moodle 3.7
*/
public static function get_async_backup_links_restore_returns() {
return new external_single_structure(
array(
'restoreurl' => new external_value(PARAM_URL, 'Restore url'),
), 'Table row data.');
}
}
+41 -7
View File
@@ -28,10 +28,20 @@ define('NO_OUTPUT_BUFFERING', true);
require_once('../config.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
// Restore of large courses requires extra memory. Use the amount configured
// in admin settings.
raise_memory_limit(MEMORY_EXTRA);
$contextid = required_param('contextid', PARAM_INT);
$stage = optional_param('stage', restore_ui::STAGE_CONFIRM, PARAM_INT);
$cancel = optional_param('cancel', '', PARAM_ALPHA);
// Determine if we are performing realtime for asynchronous backups.
$backupmode = backup::MODE_GENERAL;
if (async_helper::is_async_enabled()) {
$backupmode = backup::MODE_ASYNC;
}
list($context, $course, $cm) = get_context_info_array($contextid);
navigation_node::override_active_url(new moodle_url('/backup/restorefile.php', array('contextid'=>$contextid)));
@@ -70,10 +80,6 @@ $slowprogress->start_progress('', 10);
// This progress section counts for loading the restore controller.
$slowprogress->start_progress('', 1, 1);
// Restore of large courses requires extra memory. Use the amount configured
// in admin settings.
raise_memory_limit(MEMORY_EXTRA);
if ($stage & restore_ui::STAGE_CONFIRM + restore_ui::STAGE_DESTINATION) {
$restore = restore_ui::engage_independent_stage($stage, $contextid);
} else {
@@ -83,7 +89,7 @@ if ($stage & restore_ui::STAGE_CONFIRM + restore_ui::STAGE_DESTINATION) {
$restore = restore_ui::engage_independent_stage($stage/2, $contextid);
if ($restore->process()) {
$rc = new restore_controller($restore->get_filepath(), $restore->get_course_id(), backup::INTERACTIVE_YES,
backup::MODE_GENERAL, $USER->id, $restore->get_target());
$backupmode, $USER->id, $restore->get_target());
}
}
if ($rc) {
@@ -120,7 +126,7 @@ if (!$restore->is_independent()) {
// Use a temporary (disappearing) progress bar to show the precheck progress if any.
$precheckprogress = new \core\progress\display_if_slow(get_string('preparingdata', 'backup'));
$restore->get_controller()->set_progress($precheckprogress);
if ($restore->get_stage() == restore_ui::STAGE_PROCESS && !$restore->requires_substage()) {
if ($restore->get_stage() == restore_ui::STAGE_PROCESS && !$restore->requires_substage() && $backupmode != backup::MODE_ASYNC) {
try {
// Div used to hide the 'progress' step once the page gets onto 'finished'.
echo html_writer::start_div('', array('id' => 'executionprogress'));
@@ -150,7 +156,35 @@ if (!$restore->is_independent()) {
}
echo $renderer->progress_bar($restore->get_progress_bar());
echo $restore->display($renderer);
if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
echo $restore->display($renderer);
} else if ($restore->get_stage() == restore_ui::STAGE_PROCESS && $restore->requires_substage()) {
echo $restore->display($renderer);
} else if ($restore->get_stage() == restore_ui::STAGE_PROCESS
&& !$restore->requires_substage()
&& $backupmode == backup::MODE_ASYNC) {
// Asynchronous restore.
// Create adhoc task for restore.
$restoreid = $restore->get_restoreid();
$asynctask = new \core\task\asynchronous_restore_task();
$asynctask->set_blocking(false);
$asynctask->set_custom_data(array('backupid' => $restoreid));
\core\task\manager::queue_adhoc_task($asynctask);
// Add ajax progress bar and initiate ajax via a template.
$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
$restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid));
$progresssetup = array(
'backupid' => $restoreid,
'contextid' => $contextid,
'courseurl' => $courseurl->out(),
'restoreurl' => $restoreurl->out()
);
echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
}
$restore->destroy();
unset($restore);
+10 -2
View File
@@ -114,6 +114,7 @@ $PAGE->set_context($context);
$PAGE->set_title(get_string('course') . ': ' . $coursefullname);
$PAGE->set_heading($heading);
$PAGE->set_pagelayout('admin');
$PAGE->requires->js_call_amd('core_backup/async_backup', 'asyncBackupAllStatus', array($context->id));
$form = new course_restore_form(null, array('contextid'=>$contextid));
$data = $form->get_data();
@@ -128,8 +129,6 @@ if ($data && has_capability('moodle/restore:uploadfile', $context)) {
die;
}
echo $OUTPUT->header();
// require uploadfile cap to use file picker
@@ -196,4 +195,13 @@ if (!empty($automatedbackups)) {
echo $OUTPUT->container_end();
}
// In progress course restores.
if (async_helper::is_async_enabled()) {
echo $OUTPUT->heading_with_help(get_string('asyncrestoreinprogress', 'backup'), 'asyncrestoreinprogress', 'backup');
echo $OUTPUT->container_start();
$renderer = $PAGE->get_renderer('core', 'backup');
echo $renderer->restore_progress_viewer($USER->id, $context);
echo $OUTPUT->container_end();
}
echo $OUTPUT->footer();
+118
View File
@@ -0,0 +1,118 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Asyncronhous backup tests.
*
* @package core_backup
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->libdir . '/completionlib.php');
/**
* Asyncronhous backup tests.
*
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_backup_async_backup_testcase extends \core_privacy\tests\provider_testcase {
/**
* Tests the asynchronous backup.
*/
public function test_async_backup() {
global $DB, $CFG, $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$CFG->enableavailability = true;
$CFG->enablecompletion = true;
// Create a course with some availability data set.
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('format' => 'topics', 'numsections' => 3,
'enablecompletion' => COMPLETION_ENABLED),
array('createsections' => true));
$forum = $generator->create_module('forum', array(
'course' => $course->id));
$forum2 = $generator->create_module('forum', array(
'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
// We need a grade, easiest is to add an assignment.
$assignrow = $generator->create_module('assign', array(
'course' => $course->id));
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
$item = $assign->get_grade_item();
// Make a test grouping as well.
$grouping = $generator->create_grouping(array('courseid' => $course->id,
'name' => 'Grouping!'));
$availability = '{"op":"|","show":false,"c":[' .
'{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
'{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
'{"type":"grouping","id":' . $grouping->id . '}' .
']}';
$DB->set_field('course_modules', 'availability', $availability, array(
'id' => $forum->cmid));
$DB->set_field('course_sections', 'availability', $availability, array(
'course' => $course->id, 'section' => 1));
// Start backup process.
// Make the backup controller for an async backup.
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id);
$bc->finish_ui();
$backupid = $bc->get_backupid();
$prebackuprec = $DB->get_record('backup_controllers', array('backupid' => $backupid));
// Check the initial backup controller was created correctly.
$this->assertEquals(backup::STATUS_AWAITING, $prebackuprec->status);
$this->assertEquals(2, $prebackuprec->execution);
// Create the adhoc task.
$asynctask = new \core\task\asynchronous_backup_task();
$asynctask->set_blocking(false);
$asynctask->set_custom_data(array('backupid' => $backupid));
\core\task\manager::queue_adhoc_task($asynctask);
// We are expecting trace output during this test.
$this->expectOutputRegex("/$backupid/");
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
$postbackuprec = $DB->get_record('backup_controllers', array('backupid' => $backupid));
// Check backup was created successfully.
$this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status);
$this->assertEquals(1.0, $postbackuprec->progress);
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Asyncronhous restore tests.
*
* @package core_backup
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->libdir . '/completionlib.php');
/**
* Asyncronhous restore tests.
*
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_backup_async_restore_testcase extends \core_privacy\tests\provider_testcase {
/**
* Tests the asynchronous backup.
*/
public function test_async_restore() {
global $DB, $CFG, $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$CFG->enableavailability = true;
$CFG->enablecompletion = true;
// Create a course with some availability data set.
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('format' => 'topics', 'numsections' => 3,
'enablecompletion' => COMPLETION_ENABLED),
array('createsections' => true));
$forum = $generator->create_module('forum', array(
'course' => $course->id));
$forum2 = $generator->create_module('forum', array(
'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
// We need a grade, easiest is to add an assignment.
$assignrow = $generator->create_module('assign', array(
'course' => $course->id));
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
$item = $assign->get_grade_item();
// Make a test grouping as well.
$grouping = $generator->create_grouping(array('courseid' => $course->id,
'name' => 'Grouping!'));
$availability = '{"op":"|","show":false,"c":[' .
'{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
'{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
'{"type":"grouping","id":' . $grouping->id . '}' .
']}';
$DB->set_field('course_modules', 'availability', $availability, array(
'id' => $forum->cmid));
$DB->set_field('course_sections', 'availability', $availability, array(
'course' => $course->id, 'section' => 1));
// Backup the course.
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_YES, backup::MODE_GENERAL, $USER->id);
$bc->finish_ui();
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Get the backup file.
$coursecontext = context_course::instance($course->id);
$fs = get_file_storage();
$files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC');
$backupfile = reset($files);
// Extract backup file.
$backupdir = "restore_" . uniqid();
$path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir;
$fp = get_file_packer('application/vnd.moodle.backup');
$fp->extract_to_pathname($backupfile, $path);
// Create restore controller.
$newcourseid = restore_dbops::create_new_course(
$course->fullname, $course->shortname . '_2', $course->category);
$rc = new restore_controller($backupdir, $newcourseid,
backup::INTERACTIVE_NO, backup::MODE_ASYNC, $USER->id,
backup::TARGET_NEW_COURSE);
$this->assertTrue($rc->execute_precheck());
$restoreid = $rc->get_restoreid();
$prerestorerec = $DB->get_record('backup_controllers', array('backupid' => $restoreid));
$prerestorerec->controller = '';
$rc->destroy();
// Create the adhoc task.
$asynctask = new \core\task\asynchronous_restore_task();
$asynctask->set_blocking(false);
$asynctask->set_custom_data(array('backupid' => $restoreid));
\core\task\manager::queue_adhoc_task($asynctask);
// We are expecting trace output during this test.
$this->expectOutputRegex("/$restoreid/");
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
$postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $restoreid));
// Check backup was created successfully.
$this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status);
$this->assertEquals(1.0, $postrestorerec->progress);
}
}
@@ -641,4 +641,31 @@ abstract class backup_controller_dbops extends backup_dbops {
}
}
}
/**
* Get the progress details of a backup operation.
* Get backup records directly from database, if the backup has successfully completed
* there will be no controller object to load.
*
* @param string $backupid The backup id to query.
* @return array $progress The backup progress details.
*/
public static function get_progress($backupid) {
global $DB;
$progress = array();
$backuprecord = $DB->get_record(
'backup_controllers',
array('backupid' => $backupid),
'status, progress, operation',
MUST_EXIST);
$status = $backuprecord->status;
$progress = $backuprecord->progress;
$operation = $backuprecord->operation;
$progress = array('status' => $status, 'progress' => $progress, 'backupid' => $backupid, 'operation' => $operation);
return $progress;
}
}
+367
View File
@@ -0,0 +1,367 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Helper functions for asynchronous backups and restores.
*
* @package core
* @copyright 2019 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/user/lib.php');
/**
* Helper functions for asynchronous backups and restores.
*
* @package core
* @copyright 2019 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class async_helper {
/**
* @var string $type The type of async operation.
*/
protected $type = 'backup';
/**
* @var string $backupid The id of the backup or restore.
*/
protected $backupid;
/**
* @var object $user The user who created the backup record.
*/
protected $user;
/**
* @var object $backuprec The backup controller record from the database.
*/
protected $backuprec;
/**
* Class constructor.
*
* @param string $type The type of async operation.
* @param string $id The id of the backup or restore.
*/
public function __construct($type, $id) {
$this->type = $type;
$this->backupid = $id;
$this->backuprec = $this->get_backup_record($id);
$this->user = $this->get_user();
}
/**
* Given a backup id return a the record from the database.
* We use this method rather than 'load_controller' as the controller may
* not exist if this backup/restore has completed.
*
* @param int $id The backup id to get.
* @return object $backuprec The backup controller record.
*/
private function get_backup_record($id) {
global $DB;
$backuprec = $DB->get_record('backup_controllers', array('backupid' => $id), '*', MUST_EXIST);
return $backuprec;
}
/**
* Given a user id return a user object.
*
* @return object $user The limited user record.
*/
private function get_user() {
$userid = $this->backuprec->userid;
$user = core_user::get_user($userid, '*', MUST_EXIST);
return $user;
}
/**
* Callback for preg_replace_callback.
* Replaces message placeholders with real values.
*
* @param array $matches The match array from from preg_replace_callback.
* @return string $match The replaced string.
*/
private function lookup_message_variables($matches) {
$options = array(
'operation' => $this->type,
'backupid' => $this->backupid,
'user_username' => $this->user->username,
'user_email' => $this->user->email,
'user_firstname' => $this->user->firstname,
'user_lastname' => $this->user->lastname,
'link' => $this->get_resource_link(),
);
$match = $options[$matches[1]] ?? $matches[1];
return $match;
}
/**
* Get the link to the resource that is being backuped or restored.
*
* @return moodle_url $url The link to the resource.
*/
private function get_resource_link() {
// Get activity context only for backups.
if ($this->backuprec->type == 'activity' && $this->type == 'backup') {
$context = context_module::instance($this->backuprec->itemid);
} else { // Course or Section which have the same context getter.
$context = context_course::instance($this->backuprec->itemid);
}
// Generate link based on operation type.
if ($this->type == 'backup') {
// For backups simply generate link to restore file area UI.
$url = new moodle_url('/backup/restorefile.php', array('contextid' => $context->id));
} else {
// For restore generate link to the item itself.
$url = $context->get_url();
}
return $url;
}
/**
* Sends a confirmation message for an aynchronous process.
*
* @return int $messageid The id of the sent message.
*/
public function send_message() {
global $USER;
$subjectraw = get_config('backup', 'backup_async_message_subject');
$subjecttext = preg_replace_callback(
'/\{([-_A-Za-z0-9]+)\}/u',
array('async_helper', 'lookup_message_variables'),
$subjectraw);
$messageraw = get_config('backup', 'backup_async_message');
$messagehtml = preg_replace_callback(
'/\{([-_A-Za-z0-9]+)\}/u',
array('async_helper', 'lookup_message_variables'),
$messageraw);
$messagetext = html_to_text($messagehtml);
$message = new \core\message\message();
$message->component = 'moodle';
$message->name = 'asyncbackupnotification';
$message->userfrom = $USER;
$message->userto = $this->user;
$message->subject = $subjecttext;
$message->fullmessage = $messagetext;
$message->fullmessageformat = FORMAT_HTML;
$message->fullmessagehtml = $messagehtml;
$message->smallmessage = '';
$message->notification = '1';
$messageid = message_send($message);
return $messageid;
}
/**
* Check if asynchronous backup and restore mode is
* enabled at system level.
*
* @return bool $async True if async mode enabled false otherwise.
*/
static public function is_async_enabled() {
global $CFG;
$async = false;
if (!empty($CFG->enableasyncbackup)) {
$async = true;
}
return $async;
}
/**
* Check if there is a pending async operation for given details.
*
* @param int $id The item id to check in the backup record.
* @param string $type The type of operation: course, activity or section.
* @param string $operation Operation backup or restore.
* @return boolean $asyncpedning Is there a pending async operation.
*/
public static function is_async_pending($id, $type, $operation) {
global $DB, $USER;
$asyncpending = false;
// Only check for pending async operations if async mode is enabled.
if (self::is_async_enabled()) {
$select = 'userid = ? AND itemid = ? AND type = ? AND operation = ? AND execution = ? AND status < ? AND status > ?';
$params = array(
$USER->id,
$id,
$type,
$operation,
backup::EXECUTION_DELAYED,
backup::STATUS_FINISHED_ERR,
backup::STATUS_NEED_PRECHECK
);
$asyncpending = $DB->record_exists_select('backup_controllers', $select, $params);
}
return $asyncpending;
}
/**
* Get the size, url and restore url for a backup file.
*
* @param string $filename The name of the file to get info for.
* @param string $filearea The file area for the file.
* @param int $contextid The context ID of the file.
* @return array $results The result array containing the size, url and restore url of the file.
*/
public static function get_backup_file_info($filename, $filearea, $contextid) {
$fs = get_file_storage();
$file = $fs->get_file($contextid, 'backup', $filearea, 0, '/', $filename);
$filesize = display_size ($file->get_filesize());
$fileurl = moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
null,
$file->get_filepath(),
$file->get_filename(),
true
);
$params = array();
$params['action'] = 'choosebackupfile';
$params['filename'] = $file->get_filename();
$params['filepath'] = $file->get_filepath();
$params['component'] = $file->get_component();
$params['filearea'] = $file->get_filearea();
$params['filecontextid'] = $file->get_contextid();
$params['contextid'] = $contextid;
$params['itemid'] = $file->get_itemid();
$restoreurl = new moodle_url('/backup/restorefile.php', $params);
$filesize = display_size ($file->get_filesize());
$results = array(
'filesize' => $filesize,
'fileurl' => $fileurl->out(false),
'restoreurl' => $restoreurl->out(false));
return $results;
}
/**
* Get the url of a restored backup item based on the backup ID.
*
* @param string $backupid The backup ID to get the restore location url.
* @return array $urlarray The restored item URL as an array.
*/
public static function get_restore_url($backupid) {
global $DB;
$backupitemid = $DB->get_field('backup_controllers', 'itemid', array('backupid' => $backupid), MUST_EXIST);
$newcontext = context_course::instance($backupitemid);
$restoreurl = $newcontext->get_url()->out();
$urlarray = array('restoreurl' => $restoreurl);
return $urlarray;
}
/**
* Get markup for in progress async backups,
* to use in backup table UI.
*
* @param \core_backup_renderer $renderer The backup renderer object.
* @param integer $instanceid The context id to get backup data for.
* @return array $tabledata the rows of table data.
*/
public static function get_async_backups($renderer, $instanceid) {
global $DB;
$tabledata = array();
// Get relevant backup ids based on context instance id.
$select = 'itemid = ? AND execution = ? AND status < ? AND status > ?';
$params = array($instanceid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK);
$backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
foreach ($backups as $backup) {
$bc = \backup_controller::load_controller($backup->backupid); // Get the backup controller.
$filename = $bc->get_plan()->get_setting('filename')->get_value();
$timecreated = $backup->timecreated;
$status = $renderer->get_status_display($bc->get_status(), $bc->get_backupid());
$tablerow = array($filename, userdate($timecreated), '-', '-', '-', $status);
$tabledata[] = $tablerow;
}
return $tabledata;
}
/**
* Get the course name of the resource being restored.
*
* @param \context $context The Moodle context for the restores.
* @return string $coursename The full name of the course.
*/
public static function get_restore_name(\context $context) {
global $DB;
$instanceid = $context->instanceid;
if ($context->contextlevel == CONTEXT_MODULE) {
// For modules get the course name and module name.
$cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
$coursename = $DB->get_field('course', 'fullname', array('id' => $cm->course));
$itemname = $coursename . ' - ' . $cm->name;
} else {
$itemname = $DB->get_field('course', 'fullname', array('id' => $context->instanceid));
}
return $itemname;
}
/**
* Get all the current in progress async restores for a user.
*
* @param int $userid Moodle user id.
* @return array $restores List of current restores in progress.
*/
public static function get_async_restores($userid) {
global $DB;
$select = 'userid = ? AND execution = ? AND status < ? AND status > ? AND operation = ?';
$params = array($userid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK, 'restore');
$restores = $DB->get_records_select(
'backup_controllers',
$select,
$params,
'timecreated DESC',
'id, backupid, status, itemid, timecreated');
return $restores;
}
}
@@ -0,0 +1,148 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Asyncronhous helper tests.
*
* @package core_backup
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
/**
* Asyncronhous helper tests.
*
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_backup_async_helper_testcase extends \core_privacy\tests\provider_testcase {
/**
* Tests sending message for asynchronous backup.
*/
public function test_send_message() {
global $DB, $USER;
$this->preventResetByRollback();
$this->resetAfterTest(true);
$this->setAdminUser();
set_config('backup_async_message_users', '1', 'backup');
set_config('backup_async_message_subject', 'Moodle {operation} completed sucessfully', 'backup');
set_config('backup_async_message',
'Dear {user_firstname} {user_lastname}, <br/> Your {operation} (ID: {backupid}) has completed successfully!',
'backup');
set_config('allowedemaildomains', 'example.com');
$generator = $this->getDataGenerator();
$course = $generator->create_course(); // Create a course with some availability data set.
$user2 = $generator->create_user(array('firstname' => 'test', 'lastname' => 'human', 'maildisplay' => 1));
$generator->enrol_user($user2->id, $course->id, 'editingteacher');
$DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
set_user_preference('message_provider_moodle_asyncbackupnotification', 'email', $user2);
// Make the backup controller for an async backup.
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_YES, backup::MODE_ASYNC, $user2->id);
$bc->finish_ui();
$backupid = $bc->get_backupid();
$bc->destroy();
$sink = $this->redirectEmails();
// Send message.
$asynchelper = new async_helper('backup', $backupid);
$messageid = $asynchelper->send_message();
$emails = $sink->get_messages();
$this->assertCount(1, $emails);
$email = reset($emails);
$this->assertSame($USER->email, $email->from);
$this->assertSame($user2->email, $email->to);
$this->assertSame('Moodle backup completed sucessfully', $email->subject);
$this->assertNotEmpty($email->header);
$this->assertNotEmpty($email->body);
$this->assertRegExp("/$backupid/", $email->body);
$this->assertThat($email->body, $this->logicalNot($this->stringContains('{')));
$this->assertGreaterThan(0, $messageid);
$sink->clear();
}
/**
* Tests getting the asynchronous backup table items.
*/
public function test_get_async_backups() {
global $DB, $CFG, $USER, $PAGE;
$this->resetAfterTest(true);
$this->setAdminUser();
$CFG->enableavailability = true;
$CFG->enablecompletion = true;
// Create a course with some availability data set.
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('format' => 'topics', 'numsections' => 3,
'enablecompletion' => COMPLETION_ENABLED),
array('createsections' => true));
$forum = $generator->create_module('forum', array(
'course' => $course->id));
$forum2 = $generator->create_module('forum', array(
'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
// We need a grade, easiest is to add an assignment.
$assignrow = $generator->create_module('assign', array(
'course' => $course->id));
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
$item = $assign->get_grade_item();
// Make a test grouping as well.
$grouping = $generator->create_grouping(array('courseid' => $course->id,
'name' => 'Grouping!'));
$availability = '{"op":"|","show":false,"c":[' .
'{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
'{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
'{"type":"grouping","id":' . $grouping->id . '}' .
']}';
$DB->set_field('course_modules', 'availability', $availability, array(
'id' => $forum->cmid));
$DB->set_field('course_sections', 'availability', $availability, array(
'course' => $course->id, 'section' => 1));
// Make the backup controller for an async backup.
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id);
$bc->finish_ui();
$bc->destroy();
unset($bc);
$coursecontext = context_course::instance($course->id);
$renderer = $PAGE->get_renderer('core', 'backup');
$result = \async_helper::get_async_backups($renderer, $coursecontext->instanceid);
$this->assertEquals(1, count($result));
$this->assertEquals('backup.mbz', $result[0][0]);
}
}
+1
View File
@@ -54,6 +54,7 @@ require_once($CFG->dirroot . '/backup/util/structure/backup_nested_element.class
require_once($CFG->dirroot . '/backup/util/structure/backup_optigroup.class.php');
require_once($CFG->dirroot . '/backup/util/structure/backup_optigroup_element.class.php');
require_once($CFG->dirroot . '/backup/util/structure/backup_structure_processor.class.php');
require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/backup_general_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/backup_null_iterator.class.php');
@@ -34,6 +34,7 @@ require_once($CFG->dirroot . '/backup/util/interfaces/executable.class.php');
require_once($CFG->dirroot . '/backup/util/interfaces/processable.class.php');
require_once($CFG->dirroot . '/backup/backup.class.php');
require_once($CFG->dirroot . '/backup/util/structure/restore_path_element.class.php');
require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php');
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/ajax","core/str","core/notification","core/templates"],function(a,b,c,d,e){function f(b,c){var d=Math.round(c)+"%",e=a("#"+b+"_bar"),f=c.toFixed(2)+"%";e.attr("aria-valuenow",d),e.css("width",d),e.text(f)}function g(c){var f=a("#"+c+"_bar").parent().parent(),g=f.parent(),h=f.siblings(),i=h[1],j=a(i).text(),k=h[0],l=a(k).text();b.call([{methodname:"core_backup_get_async_backup_links_backup",args:{filename:l,contextid:n}}])[0].done(function(a){var b={filename:l,time:j,size:a.filesize,fileurl:a.fileurl,restoreurl:a.restoreurl};e.render("core/async_backup_progress_row",b).then(function(a,b){e.replaceNodeContents(g,a,b)}).fail(function(){d.exception(new Error("Failed to load table row"))})})}function h(c){var f=a("#"+c+"_bar").parent().parent(),g=f.parent(),h=f.siblings(),i=h[0],j=h[1],k=a(j).text();b.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:c,contextid:n}}])[0].done(function(b){var c=a(i).text(),f={resourcename:c,restoreurl:b.restoreurl,time:k};e.render("core/async_restore_progress_row",f).then(function(a,b){e.replaceNodeContents(g,a,b)}).fail(function(){d.exception(new Error("Failed to load table row"))})})}function i(e){var g,h=100*e.progress,i=a("#"+m+"_bar"),j=a("#"+m+"_status"),k=a("#"+m+"_detail"),l=a("#"+m+"_button");if(e.status==s){i.addClass("bg-success"),f(m,h);var r="async"+p+"processing";c.get_string(r,"backup").then(function(a){return j.text(a),a})["catch"](function(){d.exception(new Error("Failed to load string: backup "+r))})}else if(e.status==t){i.addClass("bg-danger"),i.removeClass("bg-success"),f(m,100);var v="async"+p+"error",w="async"+p+"errordetail";g=[{key:v,component:"backup"},{key:w,component:"backup"}],c.get_strings(g).then(function(a){return j.text(a[0]),k.text(a[1]),a})["catch"](function(){d.exception(new Error("Failed to load string"))}),a(".backup_progress").children("span").removeClass("backup_stage_current"),a(".backup_progress").children("span").last().addClass("backup_stage_current"),clearInterval(q)}else if(e.status==u){i.addClass("bg-success"),f(m,100);var x="async"+p+"complete";if(c.get_string(x,"backup").then(function(a){return j.text(a),a})["catch"](function(){d.exception(new Error("Failed to load string: backup "+x))}),"restore"==p)b.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:m,contextid:n}}])[0].done(function(a){var b="async"+p+"completedetail",e="async"+p+"completebutton",f=[{key:b,component:"backup",param:a.restoreurl},{key:e,component:"backup"}];c.get_strings(f).then(function(b){return k.html(b[0]),l.text(b[1]),l.attr("href",a.restoreurl),b})["catch"](function(){d.exception(new Error("Failed to load string"))})});else{var y="async"+p+"completedetail",z="async"+p+"completebutton";g=[{key:y,component:"backup",param:o},{key:z,component:"backup"}],c.get_strings(g).then(function(a){return k.html(a[0]),l.text(a[1]),l.attr("href",o),a})["catch"](function(){d.exception(new Error("Failed to load string"))})}a(".backup_progress").children("span").removeClass("backup_stage_current"),a(".backup_progress").children("span").last().addClass("backup_stage_current"),clearInterval(q)}}function j(b){b.forEach(function(b){var c=100*b.progress,d=b.backupid,e=a("#"+d+"_bar"),i=b.operation;b.status==s?(e.addClass("bg-success"),f(d,c)):b.status==t?(e.addClass("bg-danger"),e.addClass("complete"),a("#"+d+"_bar").removeClass("bg-success"),f(d,100)):b.status==u&&(e.addClass("bg-success"),e.addClass("complete"),f(d,100),"backup"==i?g(d):h(d))})}function k(){b.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:[m],contextid:n}}])[0].done(function(a){i(a[0])})}function l(){var c=[],d=a(".progress").find(".progress-bar").not(".complete");d.each(function(){c.push(this.id.substring(0,32))}),c.length>0?b.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:c,contextid:n}}])[0].done(function(a){j(a)}):clearInterval(r)}var m,n,o,p,q,r,s=800,t=900,u=1e3,v={},w=5e3;return v.asyncBackupAllStatus=function(a){n=a,r=setInterval(l,w)},v.asyncBackupStatus=function(b,c,d,e){m=b,n=c,o=d,p="backup"==e?"backup":"restore",a(".backup_progress").children("a").removeAttr("href"),q=setInterval(k,w)},v});
+424
View File
@@ -0,0 +1,424 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This module updates the UI during an asynchronous
* backup or restore process.
*
* @module backup/util/async_backup
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.7
*/
define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],
function($, ajax, Str, notification, Templates) {
/**
* Module level constants.
*
* Using var instead of const as ES6 isn't fully supported yet.
*/
var STATUS_EXECUTING = 800;
var STATUS_FINISHED_ERR = 900;
var STATUS_FINISHED_OK = 1000;
/**
* Module level variables.
*/
var Asyncbackup = {};
var checkdelay = 5000; // How often we check for progress updates.
var backupid; // The backup id to get the progress for.
var contextid; // The course this backup progress is for.
var restoreurl; // The URL to view course restores.
var typeid; // The type of operation backup or restore.
var backupintervalid; // The id of the setInterval function.
var allbackupintervalid; // The id of the setInterval function.
/**
* Helper function to update UI components.
*
* @param {string} backupid The id to match elements on.
* @param {number} percentage The completion percentage to apply.
*/
function updateElement(backupid, percentage) {
var percentagewidth = Math.round(percentage) + '%';
var elementbar = $('#' + backupid + '_bar');
var percentagetext = percentage.toFixed(2) + '%';
// Set progress bar percentage indicators
elementbar.attr('aria-valuenow', percentagewidth);
elementbar.css('width', percentagewidth);
elementbar.text(percentagetext);
}
/**
* Update backup table row when an async backup completes.
*
* @param {string} backupid The id to match elements on.
*/
function updateBackupTableRow(backupid) {
var statuscell = $('#' + backupid + '_bar').parent().parent();
var tablerow = statuscell.parent();
var cellsiblings = statuscell.siblings();
var timecell = cellsiblings[1];
var timevalue = $(timecell).text();
var filenamecell = cellsiblings[0];
var filename = $(filenamecell).text();
ajax.call([{
// Get the table data via webservice.
methodname: 'core_backup_get_async_backup_links_backup',
args: {
'filename': filename,
'contextid': contextid
},
}])[0].done(function(response) {
// We have the data now update the UI.
var context = {
filename: filename,
time: timevalue,
size: response.filesize,
fileurl: response.fileurl,
restoreurl: response.restoreurl
};
Templates.render('core/async_backup_progress_row', context).then(function(html, js) {
Templates.replaceNodeContents(tablerow, html, js);
return;
}).fail(function() {
notification.exception(new Error('Failed to load table row'));
return;
});
});
}
/**
* Update restore table row when an async restore completes.
*
* @param {string} backupid The id to match elements on.
*/
function updateRestoreTableRow(backupid) {
var statuscell = $('#' + backupid + '_bar').parent().parent();
var tablerow = statuscell.parent();
var cellsiblings = statuscell.siblings();
var coursecell = cellsiblings[0];
var timecell = cellsiblings[1];
var timevalue = $(timecell).text();
ajax.call([{
// Get the table data via webservice.
methodname: 'core_backup_get_async_backup_links_restore',
args: {
'backupid': backupid,
'contextid': contextid
},
}])[0].done(function(response) {
// We have the data now update the UI.
var resourcename = $(coursecell).text();
var context = {
resourcename: resourcename,
restoreurl: response.restoreurl,
time: timevalue
};
Templates.render('core/async_restore_progress_row', context).then(function(html, js) {
Templates.replaceNodeContents(tablerow, html, js);
return;
}).fail(function() {
notification.exception(new Error('Failed to load table row'));
return;
});
});
}
/**
* Update the Moodle user interface with the progress of
* the backup process.
*
* @param {object} progress The progress and status of the process.
*/
function updateProgress(progress) {
var percentage = progress.progress * 100;
var elementbar = $('#' + backupid + '_bar');
var elementstatus = $('#' + backupid + '_status');
var elementdetail = $('#' + backupid + '_detail');
var elementbutton = $('#' + backupid + '_button');
var stringRequests;
if (progress.status == STATUS_EXECUTING) {
// Process is in progress.
// Add in progress class color to bar
elementbar.addClass('bg-success');
updateElement(backupid, percentage);
// Change heading
var strProcessing = 'async' + typeid + 'processing';
Str.get_string(strProcessing, 'backup').then(function(title) {
elementstatus.text(title);
return title;
}).catch(function() {
notification.exception(new Error('Failed to load string: backup ' + strProcessing));
});
} else if (progress.status == STATUS_FINISHED_ERR) {
// Process completed with error.
// Add in fail class color to bar
elementbar.addClass('bg-danger');
// Remove in progress class color to bar
elementbar.removeClass('bg-success');
updateElement(backupid, 100);
// Change heading and text
var strStatus = 'async' + typeid + 'error';
var strStatusDetail = 'async' + typeid + 'errordetail';
stringRequests = [
{key: strStatus, component: 'backup'},
{key: strStatusDetail, component: 'backup'}
];
Str.get_strings(stringRequests).then(function(strings) {
elementstatus.text(strings[0]);
elementdetail.text(strings[1]);
return strings;
})
.catch(function() {
notification.exception(new Error('Failed to load string'));
return;
});
$('.backup_progress').children('span').removeClass('backup_stage_current');
$('.backup_progress').children('span').last().addClass('backup_stage_current');
// Stop checking when we either have an error or a completion.
clearInterval(backupintervalid);
} else if (progress.status == STATUS_FINISHED_OK) {
// Process completed successfully.
// Add in progress class color to bar
elementbar.addClass('bg-success');
updateElement(backupid, 100);
// Change heading and text
var strComplete = 'async' + typeid + 'complete';
Str.get_string(strComplete, 'backup').then(function(title) {
elementstatus.text(title);
return title;
}).catch(function() {
notification.exception(new Error('Failed to load string: backup ' + strComplete));
});
if (typeid == 'restore') {
ajax.call([{
// Get the table data via webservice.
methodname: 'core_backup_get_async_backup_links_restore',
args: {
'backupid': backupid,
'contextid': contextid
},
}])[0].done(function(response) {
var strDetail = 'async' + typeid + 'completedetail';
var strButton = 'async' + typeid + 'completebutton';
var stringRequests = [
{key: strDetail, component: 'backup', param: response.restoreurl},
{key: strButton, component: 'backup'}
];
Str.get_strings(stringRequests).then(function(strings) {
elementdetail.html(strings[0]);
elementbutton.text(strings[1]);
elementbutton.attr('href', response.restoreurl);
return strings;
})
.catch(function() {
notification.exception(new Error('Failed to load string'));
return;
});
});
} else {
var strDetail = 'async' + typeid + 'completedetail';
var strButton = 'async' + typeid + 'completebutton';
stringRequests = [
{key: strDetail, component: 'backup', param: restoreurl},
{key: strButton, component: 'backup'}
];
Str.get_strings(stringRequests).then(function(strings) {
elementdetail.html(strings[0]);
elementbutton.text(strings[1]);
elementbutton.attr('href', restoreurl);
return strings;
})
.catch(function() {
notification.exception(new Error('Failed to load string'));
return;
});
}
$('.backup_progress').children('span').removeClass('backup_stage_current');
$('.backup_progress').children('span').last().addClass('backup_stage_current');
// Stop checking when we either have an error or a completion.
clearInterval(backupintervalid);
}
}
/**
* Update the Moodle user interface with the progress of
* all the pending processes.
*
* @param {object} progress The progress and status of the process.
*/
function updateProgressAll(progress) {
progress.forEach(function(element) {
var percentage = element.progress * 100;
var backupid = element.backupid;
var elementbar = $('#' + backupid + '_bar');
var type = element.operation;
if (element.status == STATUS_EXECUTING) {
// Process is in element.
// Add in element class color to bar
elementbar.addClass('bg-success');
updateElement(backupid, percentage);
} else if (element.status == STATUS_FINISHED_ERR) {
// Process completed with error.
// Add in fail class color to bar
elementbar.addClass('bg-danger');
elementbar.addClass('complete');
// Remove in element class color to bar
$('#' + backupid + '_bar').removeClass('bg-success');
updateElement(backupid, 100);
} else if (element.status == STATUS_FINISHED_OK) {
// Process completed successfully.
// Add in element class color to bar
elementbar.addClass('bg-success');
elementbar.addClass('complete');
updateElement(backupid, 100);
// We have a successful backup. Update the UI with download and file details.
if (type == 'backup') {
updateBackupTableRow(backupid);
} else {
updateRestoreTableRow(backupid);
}
}
});
}
/**
* Get the progress of the backup process via ajax.
*/
function getBackupProgress() {
ajax.call([{
// Get the backup progress via webservice.
methodname: 'core_backup_get_async_backup_progress',
args: {
'backupids': [backupid],
'contextid': contextid
},
}])[0].done(function(response) {
// We have the progress now update the UI.
updateProgress(response[0]);
});
}
/**
* Get the progress of all backup processes via ajax.
*/
function getAllBackupProgress() {
var backupids = [];
var progressbars = $('.progress').find('.progress-bar').not('.complete');
progressbars.each(function() {
backupids.push((this.id).substring(0, 32));
});
if (backupids.length > 0) {
ajax.call([{
// Get the backup progress via webservice.
methodname: 'core_backup_get_async_backup_progress',
args: {
'backupids': backupids,
'contextid': contextid
},
}])[0].done(function(response) {
updateProgressAll(response);
});
} else {
clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.
}
}
/**
* Get status updates for all backups.
*
* @public
* @param {number} context The context id.
*/
Asyncbackup.asyncBackupAllStatus = function(context) {
contextid = context;
allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);
};
/**
* Get status updates for backup.
*
* @public
* @param {string} backup The backup record id.
* @param {number} context The context id.
* @param {string} restore The restore link.
* @param {string} type The operation type (backup or restore).
*/
Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {
backupid = backup;
contextid = context;
restoreurl = restore;
if (type == 'backup') {
typeid = 'backup';
} else {
typeid = 'restore';
}
// Remove the links from the progress bar, no going back now.
$('.backup_progress').children('a').removeAttr('href');
// Periodically check for progress updates and update the UI as required.
backupintervalid = setInterval(getBackupProgress, checkdelay);
};
return Asyncbackup;
});
+104 -8
View File
@@ -22,6 +22,13 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
/**
* The primary renderer for the backup.
*
@@ -64,9 +71,8 @@ class core_backup_renderer extends plugin_renderer_base {
* @return string HTML content that shows the log
*/
public function log_display($loghtml) {
global $OUTPUT;
$out = html_writer::start_div('backup_log');
$out .= $OUTPUT->heading(get_string('backuplog', 'backup'));
$out .= $this->output->heading(get_string('backuplog', 'backup'));
$out .= html_writer::start_div('backup_log_contents');
$out .= $loghtml;
$out .= html_writer::end_div();
@@ -533,6 +539,32 @@ class core_backup_renderer extends plugin_renderer_base {
return $this->render($files);
}
/**
* Generate the status indicator markup for display in the
* backup restore file area UI.
*
* @param int $statuscode The status code of the backup.
* @param string $backupid The backup record id.
* @return string|boolean $status The status indicator for the operation.
*/
public function get_status_display($statuscode, $backupid) {
if ($statuscode == backup::STATUS_AWAITING || $statuscode == backup::STATUS_EXECUTING) { // Inprogress.
$progresssetup = array(
'backupid' => $backupid,
'width' => '100'
);
$status = $this->render_from_template('core/async_backup_progress', $progresssetup);
} else if ($statuscode == backup::STATUS_FINISHED_ERR) { // Error.
$icon = $this->output->render(new \pix_icon('i/delete', get_string('failed', 'backup')));
$status = \html_writer::span($icon, 'action-icon');
} else if ($statuscode == backup::STATUS_FINISHED_OK) { // Complete.
$icon = $this->output->render(new \pix_icon('i/checked', get_string('successful', 'backup')));
$status = \html_writer::span($icon, 'action-icon');
}
return $status;
}
/**
* Displays a backup files viewer
*
@@ -544,12 +576,35 @@ class core_backup_renderer extends plugin_renderer_base {
global $CFG;
$files = $viewer->files;
$async = async_helper::is_async_enabled();
$tablehead = array(
get_string('filename', 'backup'),
get_string('time'),
get_string('size'),
get_string('download'),
get_string('restore'));
if ($async) {
$tablehead[] = get_string('status', 'backup');
}
$table = new html_table();
$table->attributes['class'] = 'backup-files-table generaltable';
$table->head = array(get_string('filename', 'backup'), get_string('time'), get_string('size'), get_string('download'), get_string('restore'));
$table->head = $tablehead;
$table->width = '100%';
$table->data = array();
// First add in progress asynchronous backups.
// Only if asynchronous backups are enabled.
// Also only render async status in correct area. Courese OR activity (not both).
if ($async
&& (($viewer->filearea == 'course' && $viewer->currentcontext->contextlevel == CONTEXT_COURSE)
|| ($viewer->filearea == 'activity' && $viewer->currentcontext->contextlevel == CONTEXT_MODULE))
) {
$table->data = \async_helper::get_async_backups($this, $viewer->currentcontext->instanceid);
}
// Add completed backups.
foreach ($files as $file) {
if ($file->is_directory()) {
continue;
@@ -585,13 +640,18 @@ class core_backup_renderer extends plugin_renderer_base {
$downloadlink = '';
}
}
$table->data[] = array(
$tabledata = array(
$file->get_filename(),
userdate($file->get_timemodified()),
display_size($file->get_filesize()),
userdate ($file->get_timemodified()),
display_size ($file->get_filesize()),
$downloadlink,
$restorelink,
);
$restorelink
);
if ($async) {
$tabledata[] = $this->get_status_display(backup::STATUS_FINISHED_OK, null);
}
$table->data[] = $tabledata;
}
$html = html_writer::table($table);
@@ -853,6 +913,42 @@ class core_backup_renderer extends plugin_renderer_base {
$output .= html_writer::end_tag('div');
return $output;
}
/**
* Get markup to render table for all of a users async
* in progress restores.
*
* @param int $userid The Moodle user id.
* @param \context $context The Moodle context for these restores.
* @return string $html The table HTML.
*/
public function restore_progress_viewer ($userid, $context) {
$tablehead = array(get_string('course'), get_string('time'), get_string('status', 'backup'));
$table = new html_table();
$table->attributes['class'] = 'backup-files-table generaltable';
$table->head = $tablehead;
$tabledata = array();
// Get all in progress async restores for this user.
$restores = \async_helper::get_async_restores($userid);
// For each backup get, new item name, time restore created and progress.
foreach ($restores as $restore) {
$restorename = \async_helper::get_restore_name($context);
$timecreated = $restore->timecreated;
$status = $this->get_status_display($restore->status, $restore->backupid);
$tablerow = array($restorename, userdate($timecreated), $status);
$tabledata[] = $tablerow;
}
$table->data = $tabledata;
$html = html_writer::table($table);
return $html;
}
}
/**
+17 -6
View File
@@ -24,6 +24,7 @@
require_once(__DIR__ . '/../config.php');
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
$id = required_param('id', PARAM_INT); // Course ID.
$delete = optional_param('delete', '', PARAM_ALPHANUM); // Confirmation hash.
@@ -74,16 +75,26 @@ if ($delete === md5($course->timemodified)) {
}
$strdeletecheck = get_string("deletecheck", "", $courseshortname);
$strdeletecoursecheck = get_string("deletecoursecheck");
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
$PAGE->navbar->add($strdeletecheck);
$PAGE->set_title("$SITE->shortname: $strdeletecheck");
$PAGE->set_heading($SITE->fullname);
echo $OUTPUT->header();
echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
// Only let user delete this course if there is not an async backup in progress.
if (!async_helper::is_async_pending($id, 'course', 'backup')) {
$strdeletecoursecheck = get_string("deletecoursecheck");
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
} else {
// Async backup is pending, don't let user delete course.
echo $OUTPUT->notification(get_string('pendingasyncerror', 'backup'), 'error');
echo $OUTPUT->container(get_string('pendingasyncdeletedetail', 'backup'));
echo $OUTPUT->continue_button($categoryurl);
}
echo $OUTPUT->footer();
exit;
+5
View File
@@ -5,6 +5,7 @@
require_once('../config.php');
require_once('lib.php');
require_once($CFG->libdir.'/completionlib.php');
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
$id = optional_param('id', 0, PARAM_INT);
$name = optional_param('name', '', PARAM_TEXT);
@@ -243,6 +244,10 @@
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
if ($USER->editing == 1 && async_helper::is_async_pending($id, 'course', 'backup')) {
echo $OUTPUT->notification(get_string('pendingasyncedit', 'backup'), 'warning');
}
if ($completion->is_enabled()) {
// This value tracks whether there has been a dynamic change to the page.
// It is used so that if a user does this - (a) set some tickmarks, (b)
+42
View File
@@ -22,6 +22,36 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['asyncbackupcomplete'] = 'The backup process has completed';
$string['asyncbackupcompletebutton'] = 'Continue';
$string['asyncbackupcompletedetail'] = 'The backup process has completed successfully completed. <br/> You can access the backup in the <a href="{$a}">restore page.</a>';
$string['asyncbackuperror'] = 'The backup process has failed';
$string['asyncbackuperrordetail'] = 'The backup process has failed. Please contact your system administrator.';
$string['asyncbackuppending'] = 'The backup process is pending';
$string['asyncbackupprocessing'] = 'The backup is in progress';
$string['asyncbadexecution'] = 'Bad backup controller execution, is {$a} should be 2';
$string['asynccheckprogress'] = ' You can check the progress at anytime at the <a href="{$a}">restore page.</a>';
$string['asyncgeneralsettings'] = 'Asynchronous backup/restore general settings';
$string['asyncemailenable'] = 'Enable message notifications';
$string['asyncemailenabledetail'] = 'When enabled users will receive a message when an asynchronous restore/backup completes';
$string['asyncmessagebody'] = 'Message';
$string['asyncmessagebodydetail'] = 'Message to send when an asynchronous restore/backup completes';
$string['asyncmessagebodydefault'] = 'Dear {user_firstname} {user_lastname}, <br/> Your {operation} (ID: {backupid}) has completed successfully! <br/><br/>You can view it here {link}.<br/>Kind Regards,<br/>Your Moodle Administrator.';
$string['asyncmessagesubject'] = 'Subject';
$string['asyncmessagesubjectdetail'] = 'Message subject';
$string['asyncmessagesubjectdefault'] = 'Moodle {operation} completed successfully';
$string['asyncnowait'] = 'You don\'t need to wait here, the process will continue in the background.';
$string['asyncprocesspending'] = 'Process pending';
$string['asyncrestorecomplete'] = 'The restore process has completed';
$string['asyncrestorecompletebutton'] = 'Continue';
$string['asyncrestorecompletedetail'] = 'The restore process has completed successfully completed. Clicking continue will take you to the <a href="{$a}">course for the restored item.</a>';
$string['asyncrestoreerror'] = 'The restore process has failed';
$string['asyncrestoreerrordetail'] = 'The restore process has failed. Please contact your system administrator.';
$string['asyncrestorepending'] = 'The restore process is pending';
$string['asyncrestoreprocessing'] = 'The restore is in progress';
$string['asyncreturn'] = 'Return to course';
$string['asyncrestoreinprogress'] = 'Restores in progress';
$string['asyncrestoreinprogress_help'] = 'Asynchronous course restores that are in progress are shown here.';
$string['autoactivedisabled'] = 'Disabled';
$string['autoactiveenabled'] = 'Enabled';
$string['autoactivemanual'] = 'Manual';
@@ -61,6 +91,7 @@ $string['backupmode30'] = 'Hub';
$string['backupmode40'] = 'Same site';
$string['backupmode50'] = 'Automated';
$string['backupmode60'] = 'Converted';
$string['backupmode70'] = 'Asynchronous';
$string['backupsection'] = 'Backup course section: {$a}';
$string['backupsettings'] = 'Backup settings';
$string['backupsitedetails'] = 'Site details';
@@ -137,6 +168,8 @@ $string['currentstage2'] = 'Schema settings';
$string['currentstage4'] = 'Confirmation and review';
$string['currentstage8'] = 'Perform backup';
$string['currentstage16'] = 'Complete';
$string['enableasyncbackup'] = 'Enable asynchronous backups';
$string['enableasyncbackup_help'] = 'If enabled, all backup and restore operations will be done asynchronously. This does not effect imports and exports. Asynchronous backups and restores allow users to do other operations while a backup or restore is in progress.';
$string['enterasearch'] = 'Enter a search';
$string['error_block_for_module_not_found'] = 'Orphan block instance (id: {$a->bid}) for course module (id: {$a->mid}) found. This block will not be backed up';
$string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) found. This module will not be backed up.';
@@ -149,6 +182,7 @@ $string['errorinvalidformat'] = 'Unknown backup format';
$string['errorinvalidformatinfo'] = 'The selected file is not a valid Moodle backup file and can\'t be restored.';
$string['errorrestorefrontpagebackup'] = 'You can only restore front page backups on the front page';
$string['executionsuccess'] = 'The backup file was successfully created.';
$string['failed'] = 'Backup failed';
$string['filename'] = 'Filename';
$string['filealiasesrestorefailures'] = 'Aliases restore failures';
$string['filealiasesrestorefailuresinfo'] = 'Some aliases included in the backup file could not be restored. The following list contains their expected location and the source file they were referring to at the original site.';
@@ -203,6 +237,7 @@ $string['importcurrentstage16'] = 'Complete';
$string['importrootsettings'] = 'Import settings';
$string['importsettings'] = 'General import settings';
$string['importsuccess'] = 'Import complete. Click continue to return to the course.';
$string['inprogress'] = 'Backup in progress';
$string['includeactivities'] = 'Include:';
$string['includeditems'] = 'Included items:';
$string['includesection'] = 'Section {$a}';
@@ -223,6 +258,10 @@ $string['nomatchingcourses'] = 'There are no courses to display';
$string['norestoreoptions'] = 'There are no categories or existing courses you can restore to.';
$string['originalwwwroot'] = 'URL of backup';
$string['overwrite'] = 'Overwrite';
$string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Muliple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
$string['pendingasyncdeletedetail'] = 'This course has an asynchronous backup pending. <br/> Courses can\'t be deleted until this backup finishes.';
$string['pendingasyncedit'] = 'There is a pending asynchronous backup for this course. Please do not edit this course until backup is complete.';
$string['pendingasyncerror'] = 'Backup pending for this resource';
$string['previousstage'] = 'Previous';
$string['preparingui'] = 'Preparing to display page';
$string['preparingdata'] = 'Preparing data';
@@ -323,6 +362,9 @@ $string['skipmodifdays'] = 'Skip courses not modified since';
$string['skipmodifdayshelp'] = 'Choose to skip courses that have not been modified since a number of days';
$string['skipmodifprev'] = 'Skip courses not modified since previous backup';
$string['skipmodifprevhelp'] = 'Choose whether to skip courses that have not been modified since the last automatic backup. This requires logging to be enabled.';
$string['status'] = 'Status';
$string['successful'] = 'Backup successful';
$string['successfulrestore'] = 'Restore successful';
$string['timetaken'] = 'Time taken';
$string['title'] = 'Title';
$string['totalcategorysearchresults'] = 'Total categories: {$a}';
+1
View File
@@ -1188,6 +1188,7 @@ $string['memberincourse'] = 'People in the course';
$string['messagebody'] = 'Message body';
$string['messagedselectedusers'] = 'Selected users have been messaged and the recipient list has been reset.';
$string['messagedselectedusersfailed'] = 'Something went wrong while messaging selected users. Some may have received the email.';
$string['messageprovider:asyncbackupnotification'] = 'Asynchronous backup/restore notifications';
$string['messageprovider:availableupdate'] = 'Available update notifications';
$string['messageprovider:backup'] = 'Backup notifications';
$string['messageprovider:badgecreatornotice'] = 'Badge creator notifications';
+123
View File
@@ -0,0 +1,123 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Progress handler that updates a database table with the progress.
*
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\progress;
defined('MOODLE_INTERNAL') || die();
/**
* Progress handler that updates a database table with the progress.
*
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class db_updater extends base {
/**
* The primary key of the database record to update.
*
* @var integer
*/
protected $recordid = 0;
/**
* The database table to insert the progress updates into.
*
* @var string
*/
protected $table = '';
/**
* The table field to update with the progress.
*
* @var string
*/
protected $field = '';
/**
* The maximum frequency in seconds to update the database (default 5 seconds).
* Lower values will increase database calls.
*
* @var integer
*/
protected $interval = 5;
/**
* The timestamp of when the next progress update to the database will be.
*
* @var integer
*/
protected $nextupdate = 0;
/**
* Constructs the progress reporter.
*
* @param int $recordid The primary key of the database record to update.
* @param string $table The databse table to insert the progress updates into.
* @param string $field The table field to update with the progress.
* @param int $interval The maximum frequency in seconds to update the database (default 5 seconds).
*/
public function __construct($recordid, $table, $field, $interval=5) {
$this->recordid = $recordid;
$this->table = $table;
$this->field = $field;
$this->interval = $interval;
}
/**
* Updates the progress in the database.
* Database update frequency is set by $interval.
*
* @see \core\progress\base::update_progress()
*/
public function update_progress() {
global $DB;
$now = $this->get_time();
$lastprogress = $this->lastprogresstime != 0 ? $this->lastprogresstime : $now;
$progressrecord = new \stdClass();
$progressrecord->id = $this->recordid;
$progressrecord->{$this->field} = '';
// Update database with progress.
if ($now > $this->nextupdate) { // Limit database updates based on time.
list ($min, $max) = $this->get_progress_proportion_range();
$progressrecord->{$this->field} = $min;
$DB->update_record($this->table, $progressrecord);
$this->nextupdate = $lastprogress + $this->interval;
}
// Set progress to 1 (100%) when there are no more progress updates.
// Their is no guarantee that the final update from the get progress method
// will be 1 even for a successful process. So we explicitly set the final DB
// value to 1 when we are no longer in progress.
if (!$this->is_in_progress_section()) {
$progressrecord->{$this->field} = 1;
$DB->update_record($this->table, $progressrecord);
}
}
}
@@ -0,0 +1,90 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Adhoc task that performs asynchronous backups.
*
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\task;
use async_helper;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
/**
* Adhoc task that performs asynchronous backups.
*
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class asynchronous_backup_task extends adhoc_task {
/**
* Run the adhoc task and preform the backup.
*/
public function execute() {
global $DB;
$started = time();
$backupid = $this->get_custom_data()->backupid;
$backuprecordid = $DB->get_field('backup_controllers', 'id', array('backupid' => $backupid), MUST_EXIST);
mtrace('Processing asynchronous backup for backup: ' . $backupid);
// Get the backup controller by backup id.
$bc = \backup_controller::load_controller($backupid);
$bc->set_progress(new \core\progress\db_updater($backuprecordid, 'backup_controllers', 'progress'));
// Do some preflight checks on the backup.
$status = $bc->get_status();
$execution = $bc->get_execution();
// Check that the backup is in the correct status and
// that is set for asynchronous execution.
if ($status == \backup::STATUS_AWAITING && $execution == \backup::EXECUTION_DELAYED) {
// Execute the backup.
$bc->execute_plan();
// Send message to user if enabled.
$messageenabled = (bool)get_config('backup', 'backup_async_message_users');
if ($messageenabled && $bc->get_status() == \backup::STATUS_FINISHED_OK) {
$asynchelper = new async_helper('backup', $backupid);
$asynchelper->send_message();
}
} else {
// If status isn't 700, it means the process has failed.
// Retrying isn't going to fix it, so marked operation as failed.
$bc->set_status(\backup::STATUS_FINISHED_ERR);
mtrace('Bad backup controller status, is: ' . $status . ' should be 700, marking job as failed.');
}
// Cleanup.
$bc->destroy();
$duration = time() - $started;
mtrace('Backup completed in: ' . $duration . ' seconds');
}
}
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Adhoc task that performs asynchronous restores.
*
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\task;
use async_helper;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
/**
* Adhoc task that performs asynchronous restores.
*
* @package core
* @copyright 2018 Matt Porritt <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class asynchronous_restore_task extends adhoc_task {
/**
* Run the adhoc task and preform the restore.
*/
public function execute() {
global $DB;
$started = time();
$restoreid = $this->get_custom_data()->backupid;
$restorerecordid = $DB->get_field('backup_controllers', 'id', array('backupid' => $restoreid), MUST_EXIST);
mtrace('Processing asynchronous restore for id: ' . $restoreid);
// Get the restore controller by backup id.
$rc = \restore_controller::load_controller($restoreid);
$rc->set_progress(new \core\progress\db_updater($restorerecordid, 'backup_controllers', 'progress'));
// Do some preflight checks on the restore.
$status = $rc->get_status();
$execution = $rc->get_execution();
// Check that the restore is in the correct status and
// that is set for asynchronous execution.
if ($status == \backup::STATUS_AWAITING && $execution == \backup::EXECUTION_DELAYED) {
// Execute the restore.
$rc->execute_plan();
// Send message to user if enabled.
$messageenabled = (bool)get_config('backup', 'backup_async_message_users');
if ($messageenabled && $rc->get_status() == \backup::STATUS_FINISHED_OK) {
$asynchelper = new async_helper('restore', $restoreid);
$asynchelper->send_message();
}
} else {
// If status isn't 700, it means the process has failed.
// Retrying isn't going to fix it, so marked operation as failed.
$rc->set_status(\backup::STATUS_FINISHED_ERR);
mtrace('Bad backup controller status, is: ' . $status . ' should be 700, marking job as failed.');
}
// Cleanup.
$rc->destroy();
$duration = time() - $started;
mtrace('Restore completed in: ' . $duration . ' seconds');
}
}
+2
View File
@@ -2898,6 +2898,7 @@
<FIELD NAME="checksum" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false" COMMENT="checksum of the backup_controller object"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="time the controller was created"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="last time the controller was modified"/>
<FIELD NAME="progress" TYPE="number" LENGTH="15" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="14" COMMENT="The backup or restore progress as a floating point number"/>
<FIELD NAME="controller" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="serialised backup_controller object"/>
</FIELDS>
<KEYS>
@@ -2907,6 +2908,7 @@
</KEYS>
<INDEXES>
<INDEX NAME="typeitem_ix" UNIQUE="false" FIELDS="type, itemid"/>
<INDEX NAME="useritem_ix" UNIQUE="false" FIELDS="userid, itemid"/>
</INDEXES>
</TABLE>
<TABLE NAME="backup_logs" COMMENT="To store all the logs from backup and restore operations (by db logger)">
+8
View File
@@ -117,4 +117,12 @@ $messageproviders = array (
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
]
],
// Asyncronhous backup/restore notifications.
'asyncbackupnotification' => array(
'defaults' => array(
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
)
),
);
+27
View File
@@ -74,6 +74,33 @@ $functions = array(
'ajax' => true,
'loginrequired' => false,
),
'core_backup_get_async_backup_progress' => array(
'classname' => 'core_backup_external',
'classpath' => 'backup/externallib.php',
'methodname' => 'get_async_backup_progress',
'description' => 'Get the progress of an Asyncronhous backup.',
'type' => 'read',
'ajax' => true,
'loginrequired' => true,
),
'core_backup_get_async_backup_links_backup' => array(
'classname' => 'core_backup_external',
'classpath' => 'backup/externallib.php',
'methodname' => 'get_async_backup_links_backup',
'description' => 'Gets the data to use when updating the status table row in the UI for when an async backup completes.',
'type' => 'read',
'ajax' => true,
'loginrequired' => true,
),
'core_backup_get_async_backup_links_restore' => array(
'classname' => 'core_backup_external',
'classpath' => 'backup/externallib.php',
'methodname' => 'get_async_backup_links_restore',
'description' => 'Gets the data to use when updating the status table row in the UI for when an async restore completes.',
'type' => 'read',
'ajax' => true,
'loginrequired' => true,
),
'core_badges_get_user_badges' => array(
'classname' => 'core_badges_external',
'methodname' => 'get_user_badges',
+18 -1
View File
@@ -2640,7 +2640,6 @@ function xmldb_main_upgrade($oldversion) {
$key = new xmldb_key('useridgroupid', XMLDB_KEY_UNIQUE, array('userid', 'groupid'));
// Launch add key useridgroupid.
$dbman->add_key($table, $key);
// Main savepoint reached.
upgrade_main_savepoint(true, 2019011801.03);
}
@@ -2934,5 +2933,23 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019040200.01);
}
if ($oldversion < 2019040800.00) {
// Define field and index to be added to backup_controllers.
$table = new xmldb_table('backup_controllers');
$field = new xmldb_field('progress', XMLDB_TYPE_NUMBER, '15, 14', null, XMLDB_NOTNULL, null, '0', 'timemodified');
$index = new xmldb_index('useritem_ix', XMLDB_INDEX_NOTUNIQUE, ['userid', 'itemid']);
// Conditionally launch add field progress.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Conditionally launch add index useritem_ix.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2019040800.00);
}
return true;
}
@@ -0,0 +1,45 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/async_backup_progress.
Moodle Asynchronous backup status template.
The purpose of this template is to render status
updates during an asynchronous backup or restore
process..
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
*
Example context (json):
{
"backupid": "f04abf8cba0319e486a3dfa7e9cb4476",
"width": "500"
}
}}
<div class="progress active" style="height: 25px; width: {{#width}}{{width}}{{/width}}{{^width}}500{{/width}}px;">
<div id="{{backupid}}_bar" class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
{{# str }} asyncprocesspending, backup {{/ str }}
</div>
</div>
@@ -0,0 +1,52 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/async_backup_progress_row.
Moodle Asynchronous backup status table row template.
The purpose of this template is to render status
table row updates during an asynchronous backup process.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
*
Example context (json):
{
"filename": "backup-moodle2-course-49390-test_course-20190326-1546-nu-nf.mbz",
"time": "Tuesday, 26 March 2019, 3:47 PM",
"size": "7.2KB",
"fileurl": "https://moodle.local/pluginfile.php/7945628/backup/course/backup-moodle2-course-49390-test_course-20190326-1546-nu-nf.mbz?forcedownload=1",
"restoreurl": "https://moodle.local/backup/restorefile.php?action=choosebackupfile&filename=backup-moodle2-course-49390-test_course-20190326-1546-nu-nf.mbz&filepath=%2F&component=backup&filearea=course&filecontextid=7945628&contextid=7945628&itemid=0"
}
}}
<td class="cell c0" style="">{{filename}}</td>
<td class="cell c1" style="">{{time}}</td>
<td class="cell c2" style="">{{size}}</td>
<td class="cell c3" style=""><a href="{{fileurl}}">{{# str }} download, core {{/ str }}</a></td>
<td class="cell c4" style=""><a href="{{restoreurl}}">{{# str }} restore, core {{/ str }}</a></td>
<td class="cell c5 lastcol" style="">
<span class="action-icon">
<i class="icon fa fa-check fa-fw " title="{{# str }} successful, backup {{/ str }}" aria-label="{{# str }} successful, backup {{/ str }}"></i>
</span>
</td>
@@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/async_backup_status.
Moodle Asynchronous backup status template.
The purpose of this template is to render status
updates during an asynchronous backup or restore
process..
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
*
Example context (json):
{
"backupid": "f04abf8cba0319e486a3dfa7e9cb4476",
"contextid": "4",
"courseurl": "/course/view.php?id=6",
"restoreurl": "/backup/restorefile.php?contextid=287",
"headingident": "backup",
"width": "500"
}
}}
<div class="progressbar_container" id="{{backupid}}">
{{#headingident}}<h3 id="{{backupid}}_status">{{# str }} asyncbackuppending, backup {{/ str }}</h3>{{/headingident}}
{{^headingident}}<h3 id="{{backupid}}_status">{{# str }} asyncrestorepending, backup {{/ str }}</h3>{{/headingident}}
{{> core/async_backup_progress }}
<p id="{{backupid}}_detail">{{# str }} asyncnowait, backup {{/ str }}<br/>{{# str }} asynccheckprogress, backup, {{restoreurl}} {{/ str }}</p>
<a id="{{backupid}}_button" href="{{courseurl}}" class="btn btn-primary">{{# str }} asyncreturn, backup {{/ str }}</a>
</div>
{{#js}}
require(['core_backup/async_backup'], function(Async) {
Async.asyncBackupStatus("{{backupid}}", "{{contextid}}", "{{restoreurl}}", "{{headingident}}");
});
{{/js}}
@@ -0,0 +1,51 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/async_restore_progress_row.
Moodle Asynchronous restore status table row template.
The purpose of this template is to render status
table row updates during an asynchronous restore process.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
*
Example context (json):
{
"resourcename" : "Mathematics 101",
"time": "Tuesday, 26 March 2019, 3:47 PM",
"restoreurl": "https://moodle.local/backup/restorefile.php?action=choosebackupfile&filename=backup-moodle2-course-49390-test_course-20190326-1546-nu-nf.mbz&filepath=%2F&component=backup&filearea=course&filecontextid=7945628&contextid=7945628&itemid=0"
}
}}
<td class="cell c0" style="">
<a href="{{restoreurl}}">{{resourcename}}</a>
</td>
<td class="cell c1" style="">{{time}}</td>
<td class="cell c2 lastcol" style="">
<span class="action-icon">
<i class="icon fa fa-check fa-fw " title="{{# str }} successfulrestore, backup {{/ str }}" aria-label="{{# str }} successfulrestore, backup {{/ str }}"></i>
</span>
</td>
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2019040600.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2019040800.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.