Merge branch 'master_MDL-28505_Asynchronous_backup_and_restore' of https://github.com/mattporritt/moodle

This commit is contained in:
Adrian Greeve 2019-04-09 10:00:23 +08:00
commit 29bc29ed4f
35 changed files with 2459 additions and 115 deletions

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);
}
}

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));
}

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;

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();

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;
}
}
/*

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
backup/externallib.php Normal file
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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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.');
}
}

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);

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();

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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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);
}
}

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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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);
}
}

View File

@ -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;
}
}

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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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;
}
}

View File

@ -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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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]);
}
}

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');

View File

@ -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');

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});

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 <mattp@catalyst-au.net>
* @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;
});

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;
}
}
/**

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;

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)

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}';

View File

@ -1190,6 +1190,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';

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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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);
}
}
}

View File

@ -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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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');
}
}

View File

@ -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 <mattp@catalyst-au.net>
* @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 <mattp@catalyst-au.net>
* @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');
}
}

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)">

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,
)
),
);

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',

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);
}
@ -2956,5 +2955,23 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019040600.02);
}
if ($oldversion < 2019040600.04) {
// 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, 2019040600.04);
}
return true;
}

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2019040600.03; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2019040600.04; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.