MDL-19711 dml: Enable use of readonly slave database handles
[moodle.git] / backup / util / ui / amd / src / async_backup.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * This module updates the UI during an asynchronous
18  * backup or restore process.
19  *
20  * @module     backup/util/async_backup
21  * @package    core
22  * @copyright  2018 Matt Porritt <mattp@catalyst-au.net>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      3.7
25  */
26 define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],
27         function($, ajax, Str, notification, Templates) {
29     /**
30      * Module level constants.
31      *
32      * Using var instead of const as ES6 isn't fully supported yet.
33      */
34     var STATUS_EXECUTING = 800;
35     var STATUS_FINISHED_ERR = 900;
36     var STATUS_FINISHED_OK = 1000;
38     /**
39      * Module level variables.
40      */
41     var Asyncbackup = {};
42     var checkdelayoriginal = 15000; // This is the default time to use.
43     var checkdelay = 15000; // How often we should check for progress updates.
44     var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value
45     var backupid; //  The backup id to get the progress for.
46     var contextid; //  The course this backup progress is for.
47     var restoreurl; //  The URL to view course restores.
48     var typeid; //  The type of operation backup or restore.
49     var backupintervalid; //  The id of the setInterval function.
50     var allbackupintervalid; //  The id of the setInterval function.
51     var timeout = 2000; // Timeout for ajax requests.
53     /**
54      * Helper function to update UI components.
55      *
56      * @param {string} backupid The id to match elements on.
57      * @param {number} percentage The completion percentage to apply.
58      */
59     function updateElement(backupid, percentage) {
60         var percentagewidth = Math.round(percentage) + '%';
61         var elementbar = $('#' + backupid + '_bar');
62         var percentagetext = percentage.toFixed(2) + '%';
64         // Set progress bar percentage indicators
65         elementbar.attr('aria-valuenow', percentagewidth);
66         elementbar.css('width', percentagewidth);
67         elementbar.text(percentagetext);
68     }
70     /**
71      * Updates the interval we use to check for backup progress.
72      *
73      * @param {Number} intervalid The id of the interval
74      * @param {Function} callback The function to use in setInterval
75      * @param {Number} value The specified interval (in milliseconds)
76      * @returns {Number}
77      */
78     function updateInterval(intervalid, callback, value) {
79         clearInterval(intervalid);
80         return setInterval(callback, value);
81     }
83     /**
84      * Update backup table row when an async backup completes.
85      *
86      * @param {string} backupid The id to match elements on.
87      */
88     function updateBackupTableRow(backupid) {
89         var statuscell = $('#' + backupid + '_bar').parent().parent();
90         var tablerow = statuscell.parent();
91         var cellsiblings = statuscell.siblings();
92         var timecell = cellsiblings[1];
93         var timevalue = $(timecell).text();
94         var filenamecell = cellsiblings[0];
95         var filename = $(filenamecell).text();
97         ajax.call([{
98             // Get the table data via webservice.
99             methodname: 'core_backup_get_async_backup_links_backup',
100             args: {
101                 'filename': filename,
102                 'contextid': contextid
103             },
104         }])[0].done(function(response) {
105             // We have the data now update the UI.
106             var context = {
107                     filename: filename,
108                     time: timevalue,
109                     size: response.filesize,
110                     fileurl: response.fileurl,
111                     restoreurl: response.restoreurl
112                     };
114             Templates.render('core/async_backup_progress_row', context).then(function(html, js) {
115                 Templates.replaceNodeContents(tablerow, html, js);
116                 return;
117             }).fail(function() {
118                 notification.exception(new Error('Failed to load table row'));
119                 return;
120             });
121         });
122     }
124     /**
125      * Update restore table row when an async restore completes.
126      *
127      * @param {string} backupid The id to match elements on.
128      */
129     function updateRestoreTableRow(backupid) {
130         var statuscell = $('#' + backupid + '_bar').parent().parent();
131         var tablerow = statuscell.parent();
132         var cellsiblings = statuscell.siblings();
133         var coursecell = cellsiblings[0];
134         var timecell = cellsiblings[1];
135         var timevalue = $(timecell).text();
137         ajax.call([{
138             // Get the table data via webservice.
139             methodname: 'core_backup_get_async_backup_links_restore',
140             args: {
141                 'backupid': backupid,
142                 'contextid': contextid
143             },
144         }])[0].done(function(response) {
145          // We have the data now update the UI.
146             var resourcename = $(coursecell).text();
147             var context = {
148                     resourcename: resourcename,
149                     restoreurl: response.restoreurl,
150                     time: timevalue
151                     };
153             Templates.render('core/async_restore_progress_row', context).then(function(html, js) {
154                 Templates.replaceNodeContents(tablerow, html, js);
155                 return;
156             }).fail(function() {
157                 notification.exception(new Error('Failed to load table row'));
158                 return;
159             });
160         });
161     }
163     /**
164      * Update the Moodle user interface with the progress of
165      * the backup process.
166      *
167      * @param {object} progress The progress and status of the process.
168      */
169     function updateProgress(progress) {
170         var percentage = progress.progress * 100;
171         var elementbar = $('#' + backupid + '_bar');
172         var elementstatus = $('#' + backupid + '_status');
173         var elementdetail = $('#' + backupid + '_detail');
174         var elementbutton = $('#' + backupid + '_button');
175         var stringRequests;
177         if (progress.status == STATUS_EXECUTING) {
178             // Process is in progress.
179             // Add in progress class color to bar
180             elementbar.addClass('bg-success');
182             updateElement(backupid, percentage);
184             // Change heading
185             var strProcessing = 'async' + typeid + 'processing';
186             Str.get_string(strProcessing, 'backup').then(function(title) {
187                 elementstatus.text(title);
188                 return title;
189             }).catch(function() {
190                 notification.exception(new Error('Failed to load string: backup ' + strProcessing));
191             });
193         } else if (progress.status == STATUS_FINISHED_ERR) {
194             // Process completed with error.
196             // Add in fail class color to bar
197             elementbar.addClass('bg-danger');
199             // Remove in progress class color to bar
200             elementbar.removeClass('bg-success');
202             updateElement(backupid, 100);
204             // Change heading and text
205             var strStatus = 'async' + typeid + 'error';
206             var strStatusDetail = 'async' + typeid + 'errordetail';
207             stringRequests = [
208                 {key: strStatus, component: 'backup'},
209                 {key: strStatusDetail, component: 'backup'}
210             ];
211             Str.get_strings(stringRequests).then(function(strings) {
212                 elementstatus.text(strings[0]);
213                 elementdetail.text(strings[1]);
215                 return strings;
216             })
217             .catch(function() {
218                 notification.exception(new Error('Failed to load string'));
219                 return;
220             });
222             $('.backup_progress').children('span').removeClass('backup_stage_current');
223             $('.backup_progress').children('span').last().addClass('backup_stage_current');
225             // Stop checking when we either have an error or a completion.
226             clearInterval(backupintervalid);
228         } else if (progress.status == STATUS_FINISHED_OK) {
229             // Process completed successfully.
231             // Add in progress class color to bar
232             elementbar.addClass('bg-success');
234             updateElement(backupid, 100);
236             // Change heading and text
237             var strComplete = 'async' + typeid + 'complete';
238             Str.get_string(strComplete, 'backup').then(function(title) {
239                 elementstatus.text(title);
240                 return title;
241             }).catch(function() {
242                 notification.exception(new Error('Failed to load string: backup ' + strComplete));
243             });
245             if (typeid == 'restore') {
246                 ajax.call([{
247                     // Get the table data via webservice.
248                     methodname: 'core_backup_get_async_backup_links_restore',
249                     args: {
250                         'backupid': backupid,
251                         'contextid': contextid
252                     },
253                 }])[0].done(function(response) {
254                     var strDetail = 'async' + typeid + 'completedetail';
255                     var strButton = 'async' + typeid + 'completebutton';
256                     var stringRequests = [
257                         {key: strDetail, component: 'backup', param: response.restoreurl},
258                         {key: strButton, component: 'backup'}
259                     ];
260                     Str.get_strings(stringRequests).then(function(strings) {
261                         elementdetail.html(strings[0]);
262                         elementbutton.text(strings[1]);
263                         elementbutton.attr('href', response.restoreurl);
265                         return strings;
266                     })
267                     .catch(function() {
268                         notification.exception(new Error('Failed to load string'));
269                         return;
270                     });
272                 });
273             } else {
274                 var strDetail = 'async' + typeid + 'completedetail';
275                 var strButton = 'async' + typeid + 'completebutton';
276                 stringRequests = [
277                     {key: strDetail, component: 'backup', param: restoreurl},
278                     {key: strButton, component: 'backup'}
279                 ];
280                 Str.get_strings(stringRequests).then(function(strings) {
281                     elementdetail.html(strings[0]);
282                     elementbutton.text(strings[1]);
283                     elementbutton.attr('href', restoreurl);
285                     return strings;
286                 })
287                 .catch(function() {
288                     notification.exception(new Error('Failed to load string'));
289                     return;
290                 });
292             }
294             $('.backup_progress').children('span').removeClass('backup_stage_current');
295             $('.backup_progress').children('span').last().addClass('backup_stage_current');
297             // Stop checking when we either have an error or a completion.
298             clearInterval(backupintervalid);
299         }
300     }
302     /**
303      * Update the Moodle user interface with the progress of
304      * all the pending processes.
305      *
306      * @param {object} progress The progress and status of the process.
307      */
308     function updateProgressAll(progress) {
309         progress.forEach(function(element) {
310             var percentage = element.progress * 100;
311             var backupid = element.backupid;
312             var elementbar = $('#' + backupid + '_bar');
313             var type = element.operation;
315             if (element.status == STATUS_EXECUTING) {
316                 // Process is in element.
318                 // Add in element class color to bar
319                 elementbar.addClass('bg-success');
321                 updateElement(backupid, percentage);
323             } else if (element.status == STATUS_FINISHED_ERR) {
324                 // Process completed with error.
326                 // Add in fail class color to bar
327                 elementbar.addClass('bg-danger');
328                 elementbar.addClass('complete');
330                 // Remove in element class color to bar
331                 $('#' + backupid + '_bar').removeClass('bg-success');
333                 updateElement(backupid, 100);
335             } else if (element.status == STATUS_FINISHED_OK) {
336                 // Process completed successfully.
338                 // Add in element class color to bar
339                 elementbar.addClass('bg-success');
340                 elementbar.addClass('complete');
342                 updateElement(backupid, 100);
344                 // We have a successful backup. Update the UI with download and file details.
345                 if (type == 'backup') {
346                     updateBackupTableRow(backupid);
347                 } else {
348                     updateRestoreTableRow(backupid);
349                 }
351             }
353         });
354     }
356     /**
357      * Get the progress of the backup process via ajax.
358      */
359     function getBackupProgress() {
360         ajax.call([{
361             // Get the backup progress via webservice.
362             methodname: 'core_backup_get_async_backup_progress',
363             args: {
364                 'backupids': [backupid],
365                 'contextid': contextid
366             },
367         }], true, true, false, timeout)[0].done(function(response) {
368             // We have the progress now update the UI.
369             updateProgress(response[0]);
370             checkdelay = checkdelayoriginal;
371             backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal);
372         }).fail(function() {
373             checkdelay = checkdelay * checkdelaymultipler;
374             backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay);
375         });
376     }
378     /**
379      * Get the progress of all backup processes via ajax.
380      */
381     function getAllBackupProgress() {
382         var backupids = [];
383         var progressbars = $('.progress').find('.progress-bar').not('.complete');
385         progressbars.each(function() {
386             backupids.push((this.id).substring(0, 32));
387         });
389         if (backupids.length > 0) {
390             ajax.call([{
391                 // Get the backup progress via webservice.
392                 methodname: 'core_backup_get_async_backup_progress',
393                 args: {
394                     'backupids': backupids,
395                     'contextid': contextid
396                 },
397             }], true, true, false, timeout)[0].done(function(response) {
398                 updateProgressAll(response);
399                 checkdelay = checkdelayoriginal;
400                 allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal);
401             }).fail(function() {
402                 checkdelay = checkdelay * checkdelaymultipler;
403                 allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay);
404             });
405         } else {
406             clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.
407         }
408     }
410     /**
411      * Get status updates for all backups.
412      *
413      * @public
414      * @param {number} context The context id.
415      */
416     Asyncbackup.asyncBackupAllStatus = function(context) {
417         contextid = context;
418         allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);
419     };
421     /**
422      * Get status updates for backup.
423      *
424      * @public
425      * @param {string} backup The backup record id.
426      * @param {number} context The context id.
427      * @param {string} restore The restore link.
428      * @param {string} type The operation type (backup or restore).
429      */
430     Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {
431         backupid = backup;
432         contextid = context;
433         restoreurl = restore;
435         if (type == 'backup') {
436             typeid = 'backup';
437         } else {
438             typeid = 'restore';
439         }
441         // Remove the links from the progress bar, no going back now.
442         $('.backup_progress').children('a').removeAttr('href');
444         //  Periodically check for progress updates and update the UI as required.
445         backupintervalid = setInterval(getBackupProgress, checkdelay);
447       };
449       return Asyncbackup;
450 });