mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 12:32:08 +02:00
Merge branch 'master_MDL-28505_Asynchronous_backup_and_restore' of https://github.com/mattporritt/moodle
This commit is contained in:
commit
29bc29ed4f
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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
240
backup/externallib.php
Normal 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.');
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -114,6 +114,7 @@ $PAGE->set_context($context);
|
||||
$PAGE->set_title(get_string('course') . ': ' . $coursefullname);
|
||||
$PAGE->set_heading($heading);
|
||||
$PAGE->set_pagelayout('admin');
|
||||
$PAGE->requires->js_call_amd('core_backup/async_backup', 'asyncBackupAllStatus', array($context->id));
|
||||
|
||||
$form = new course_restore_form(null, array('contextid'=>$contextid));
|
||||
$data = $form->get_data();
|
||||
@ -128,8 +129,6 @@ if ($data && has_capability('moodle/restore:uploadfile', $context)) {
|
||||
die;
|
||||
}
|
||||
|
||||
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
// require uploadfile cap to use file picker
|
||||
@ -196,4 +195,13 @@ if (!empty($automatedbackups)) {
|
||||
echo $OUTPUT->container_end();
|
||||
}
|
||||
|
||||
// In progress course restores.
|
||||
if (async_helper::is_async_enabled()) {
|
||||
echo $OUTPUT->heading_with_help(get_string('asyncrestoreinprogress', 'backup'), 'asyncrestoreinprogress', 'backup');
|
||||
echo $OUTPUT->container_start();
|
||||
$renderer = $PAGE->get_renderer('core', 'backup');
|
||||
echo $renderer->restore_progress_viewer($USER->id, $context);
|
||||
echo $OUTPUT->container_end();
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
118
backup/tests/async_backup_test.php
Normal file
118
backup/tests/async_backup_test.php
Normal 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);
|
||||
}
|
||||
}
|
140
backup/tests/async_restore_test.php
Normal file
140
backup/tests/async_restore_test.php
Normal 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);
|
||||
}
|
||||
}
|
@ -641,4 +641,31 @@ abstract class backup_controller_dbops extends backup_dbops {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the progress details of a backup operation.
|
||||
* Get backup records directly from database, if the backup has successfully completed
|
||||
* there will be no controller object to load.
|
||||
*
|
||||
* @param string $backupid The backup id to query.
|
||||
* @return array $progress The backup progress details.
|
||||
*/
|
||||
public static function get_progress($backupid) {
|
||||
global $DB;
|
||||
|
||||
$progress = array();
|
||||
$backuprecord = $DB->get_record(
|
||||
'backup_controllers',
|
||||
array('backupid' => $backupid),
|
||||
'status, progress, operation',
|
||||
MUST_EXIST);
|
||||
|
||||
$status = $backuprecord->status;
|
||||
$progress = $backuprecord->progress;
|
||||
$operation = $backuprecord->operation;
|
||||
|
||||
$progress = array('status' => $status, 'progress' => $progress, 'backupid' => $backupid, 'operation' => $operation);
|
||||
|
||||
return $progress;
|
||||
}
|
||||
}
|
||||
|
367
backup/util/helper/async_helper.class.php
Normal file
367
backup/util/helper/async_helper.class.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
148
backup/util/helper/tests/async_helper_test.php
Normal file
148
backup/util/helper/tests/async_helper_test.php
Normal 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]);
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ require_once($CFG->dirroot . '/backup/util/structure/backup_nested_element.class
|
||||
require_once($CFG->dirroot . '/backup/util/structure/backup_optigroup.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/structure/backup_optigroup_element.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/structure/backup_structure_processor.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/backup_general_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/backup_null_iterator.class.php');
|
||||
|
@ -34,6 +34,7 @@ require_once($CFG->dirroot . '/backup/util/interfaces/executable.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/interfaces/processable.class.php');
|
||||
require_once($CFG->dirroot . '/backup/backup.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/structure/restore_path_element.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php');
|
||||
|
1
backup/util/ui/amd/build/async_backup.min.js
vendored
Normal file
1
backup/util/ui/amd/build/async_backup.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
define(["jquery","core/ajax","core/str","core/notification","core/templates"],function(a,b,c,d,e){function f(b,c){var d=Math.round(c)+"%",e=a("#"+b+"_bar"),f=c.toFixed(2)+"%";e.attr("aria-valuenow",d),e.css("width",d),e.text(f)}function g(c){var f=a("#"+c+"_bar").parent().parent(),g=f.parent(),h=f.siblings(),i=h[1],j=a(i).text(),k=h[0],l=a(k).text();b.call([{methodname:"core_backup_get_async_backup_links_backup",args:{filename:l,contextid:n}}])[0].done(function(a){var b={filename:l,time:j,size:a.filesize,fileurl:a.fileurl,restoreurl:a.restoreurl};e.render("core/async_backup_progress_row",b).then(function(a,b){e.replaceNodeContents(g,a,b)}).fail(function(){d.exception(new Error("Failed to load table row"))})})}function h(c){var f=a("#"+c+"_bar").parent().parent(),g=f.parent(),h=f.siblings(),i=h[0],j=h[1],k=a(j).text();b.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:c,contextid:n}}])[0].done(function(b){var c=a(i).text(),f={resourcename:c,restoreurl:b.restoreurl,time:k};e.render("core/async_restore_progress_row",f).then(function(a,b){e.replaceNodeContents(g,a,b)}).fail(function(){d.exception(new Error("Failed to load table row"))})})}function i(e){var g,h=100*e.progress,i=a("#"+m+"_bar"),j=a("#"+m+"_status"),k=a("#"+m+"_detail"),l=a("#"+m+"_button");if(e.status==s){i.addClass("bg-success"),f(m,h);var r="async"+p+"processing";c.get_string(r,"backup").then(function(a){return j.text(a),a})["catch"](function(){d.exception(new Error("Failed to load string: backup "+r))})}else if(e.status==t){i.addClass("bg-danger"),i.removeClass("bg-success"),f(m,100);var v="async"+p+"error",w="async"+p+"errordetail";g=[{key:v,component:"backup"},{key:w,component:"backup"}],c.get_strings(g).then(function(a){return j.text(a[0]),k.text(a[1]),a})["catch"](function(){d.exception(new Error("Failed to load string"))}),a(".backup_progress").children("span").removeClass("backup_stage_current"),a(".backup_progress").children("span").last().addClass("backup_stage_current"),clearInterval(q)}else if(e.status==u){i.addClass("bg-success"),f(m,100);var x="async"+p+"complete";if(c.get_string(x,"backup").then(function(a){return j.text(a),a})["catch"](function(){d.exception(new Error("Failed to load string: backup "+x))}),"restore"==p)b.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:m,contextid:n}}])[0].done(function(a){var b="async"+p+"completedetail",e="async"+p+"completebutton",f=[{key:b,component:"backup",param:a.restoreurl},{key:e,component:"backup"}];c.get_strings(f).then(function(b){return k.html(b[0]),l.text(b[1]),l.attr("href",a.restoreurl),b})["catch"](function(){d.exception(new Error("Failed to load string"))})});else{var y="async"+p+"completedetail",z="async"+p+"completebutton";g=[{key:y,component:"backup",param:o},{key:z,component:"backup"}],c.get_strings(g).then(function(a){return k.html(a[0]),l.text(a[1]),l.attr("href",o),a})["catch"](function(){d.exception(new Error("Failed to load string"))})}a(".backup_progress").children("span").removeClass("backup_stage_current"),a(".backup_progress").children("span").last().addClass("backup_stage_current"),clearInterval(q)}}function j(b){b.forEach(function(b){var c=100*b.progress,d=b.backupid,e=a("#"+d+"_bar"),i=b.operation;b.status==s?(e.addClass("bg-success"),f(d,c)):b.status==t?(e.addClass("bg-danger"),e.addClass("complete"),a("#"+d+"_bar").removeClass("bg-success"),f(d,100)):b.status==u&&(e.addClass("bg-success"),e.addClass("complete"),f(d,100),"backup"==i?g(d):h(d))})}function k(){b.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:[m],contextid:n}}])[0].done(function(a){i(a[0])})}function l(){var c=[],d=a(".progress").find(".progress-bar").not(".complete");d.each(function(){c.push(this.id.substring(0,32))}),c.length>0?b.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:c,contextid:n}}])[0].done(function(a){j(a)}):clearInterval(r)}var m,n,o,p,q,r,s=800,t=900,u=1e3,v={},w=5e3;return v.asyncBackupAllStatus=function(a){n=a,r=setInterval(l,w)},v.asyncBackupStatus=function(b,c,d,e){m=b,n=c,o=d,p="backup"==e?"backup":"restore",a(".backup_progress").children("a").removeAttr("href"),q=setInterval(k,w)},v});
|
424
backup/util/ui/amd/src/async_backup.js
Normal file
424
backup/util/ui/amd/src/async_backup.js
Normal 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;
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,6 +24,7 @@
|
||||
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
|
||||
$id = required_param('id', PARAM_INT); // Course ID.
|
||||
$delete = optional_param('delete', '', PARAM_ALPHANUM); // Confirmation hash.
|
||||
@ -74,16 +75,26 @@ if ($delete === md5($course->timemodified)) {
|
||||
}
|
||||
|
||||
$strdeletecheck = get_string("deletecheck", "", $courseshortname);
|
||||
$strdeletecoursecheck = get_string("deletecoursecheck");
|
||||
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
|
||||
|
||||
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
|
||||
$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
|
||||
|
||||
$PAGE->navbar->add($strdeletecheck);
|
||||
$PAGE->set_title("$SITE->shortname: $strdeletecheck");
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
|
||||
|
||||
// Only let user delete this course if there is not an async backup in progress.
|
||||
if (!async_helper::is_async_pending($id, 'course', 'backup')) {
|
||||
$strdeletecoursecheck = get_string("deletecoursecheck");
|
||||
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
|
||||
|
||||
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
|
||||
$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
|
||||
echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
|
||||
} else {
|
||||
// Async backup is pending, don't let user delete course.
|
||||
echo $OUTPUT->notification(get_string('pendingasyncerror', 'backup'), 'error');
|
||||
echo $OUTPUT->container(get_string('pendingasyncdeletedetail', 'backup'));
|
||||
echo $OUTPUT->continue_button($categoryurl);
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
|
@ -5,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)
|
||||
|
@ -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}';
|
||||
|
@ -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';
|
||||
|
123
lib/classes/progress/db_updater.php
Normal file
123
lib/classes/progress/db_updater.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
90
lib/classes/task/asynchronous_backup_task.php
Normal file
90
lib/classes/task/asynchronous_backup_task.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
89
lib/classes/task/asynchronous_restore_task.php
Normal file
89
lib/classes/task/asynchronous_restore_task.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
@ -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)">
|
||||
|
@ -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,
|
||||
)
|
||||
),
|
||||
);
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
|
45
lib/templates/async_backup_progress.mustache
Normal file
45
lib/templates/async_backup_progress.mustache
Normal 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>
|
52
lib/templates/async_backup_progress_row.mustache
Normal file
52
lib/templates/async_backup_progress_row.mustache
Normal 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>
|
57
lib/templates/async_backup_status.mustache
Normal file
57
lib/templates/async_backup_status.mustache
Normal 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}}
|
51
lib/templates/async_restore_progress_row.mustache
Normal file
51
lib/templates/async_restore_progress_row.mustache
Normal 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>
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user